并查集
这篇文章主要讲讲 Union Find1 算法, 也就是我们常说的并查集算法。主要用于图算法中的 动态连通性 。
动态连通性¶
Note
假设输入一连串的整数对,其中一对整数对p
和q
,它们分别代表不同类型的对象,这对整数对p
和q
是相连的。则这两个对象可以属于同一个连通分量,而再接收之后的整数对时,则可以判断整数对中的两个元素是否属于同一个连通分量,来过滤掉无意义的整数对,因为如果整数对中的两个元素在同一个连通分量中,则可以肯定的是它们一定是相连通的。
简单来说,这个动态连通性,可以想象成一个图结构,图中的节点有些互相连接,有些不互相连接,如果两个元素相连,则它们具有以下特性:
- Symmetric (对称性): 如果
p
与q
连接,则q
与p
也相连 - Transitive (传递性): 如果
p
与q
相连,q
与r
相连,则p
与r
也相连 - Reflexive (自反性): 自身与自身相连
而动态连通性的目标就是要找出无意义的pair,例如一个整数对p
与q
,如果之前的整数对已经表明他们是相连接的了,那我们就忽略这个整数对,继续处理下一个整数对。
而Union Find主要有以下几个实现:
Union Find Pseudo Code | |
---|---|
1 2 3 4 5 6 7 |
|
Quick Union 实现¶
在此之前有一个 Quick Find 算法,这个算法中union
操作每次都会访问一次数组,所以当连通分量数量很小时,最终的算法时间是平方及的,所以不在这讨论了
思路¶
详细的过程可以阅读一下普林斯顿大学的 cos2262 中讲 Union Find 的 slides,这里我们简单理解一下上面的API。
如果两个节点在不同的分量中,union()
操作会将两个分量合并。find()
操作会找到给定节点的连通分量标识(也可以理解为它的根节点),connected()
操作会找到给定的两个节点的连通分量标识,如果两个连通分量标识相同,则说明两个节点是相连的。count()
操作返回了当前连通分量的数量。初始化时,我们有N
个连通分量,当操作union()
的时候,连通分量的数量就会减一。
实际上,我们在算法中维护两个变量就可以实现:parent[]数组(代表连通分量标识,因为union()
实际上是将一个节点的根节点设置为另一个节点根节点的字节点),还有一个是count
,代表连通分量的数量,初始化时,count
的数量为N。
代码¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 |
|
可以参照下面的流程图:
算法分析¶
那么上面实现的这个算法的复杂度是多少呢?从代码上可以看到:connected
和union
的复杂度都和find
函数有关,而find
函数则是需要从该节点向上寻找到这个节点的根节点,虽然它是树形结构,但我们不能单纯的说它的时间复杂度是logN
(平衡二叉树的高度),但这个可不是平衡二叉树,在极端情况下,这个树形结构可能就会退化成单链表,所以说最坏情况下的时间复杂度可能变成N
。
所以说上面这种写法,时间复杂度是O(N)
,这个复杂度是很不理想的。因为图论解决的都是数据规模很大的问题,对于union
和connected
的调用非常频繁,所以线性的时间复杂度效率是不可接受的。
现在的问题就在于如何去避免树的不平衡。
加权 Quick Union 算法¶
我们只需要简单的修改一下 Quick Union 算法 就能保证这种情况不再出现,只要每次union
操作时,总是把小的树合并到大的树上,就可以解决这样的问题。所以我们需要一个额外的数组,来记录每个连通分量的大小。
代码实现¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 |
|
我们通过加入了由节点索引的实例变量数组size[]
,这样union
操作就能将小树的根节点,连接到大树的根节点,这样算法就能处理数据规模较大的问题了。
我们来看看 Union Find 算法各种实现的性能:
算法 | 构造函数 | union() | find() |
---|---|---|---|
quick-find 算法 | N | N | 1 |
quick-union 算法 | N | 树的高度,最坏为 N | 树的高度,最坏为 N |
加权 quick-union 算法 | N | lgN | lgN |
加权 quick-union 算法 | N | 非常接近但没有到达1 |
思考¶
这样看来,加权 Quick Union 算法的时间复杂度为lgN
,看起来是比较理想的状态了,那有没有比这个更好的呢?有没有能达到常数级别的算法呢?
路径压缩的加权 Quick Union 算法¶
路径压缩就是将一个连通分量中的所有节点,都直接连接到根节点上,使树的高度不超过2
changes in find() | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 |
|
这样实现的话,理想情况下的时间复杂度是1
,不过几乎不可能达到,只能是接近1的时间复杂度。
提高¶
- 使用树结构(TreeNode)来实现并查集
引用¶
- Union Find Slides 23