Union Find (Disjoint Sets) Algorithm

update Sep 7, 2017 18:06

Introduction:

这也就是传说中的并查集,在CLRS中被叫做 Disjoint Set。这是一种用来解决动态连通性问题的算法,主要可以实现如下三个接口:

  1. makeSet: 初始化一个并查集,此时并查集中每一个 node 都是独立为自身的代表,也就是说此时每个集合都只有唯一一个元素;

  2. find(node): 在并查集中查找某元素所归属集合的representation并返回,可以用 find(node1)==find(node2) 来判断两个元素是否在同一个集合;

  3. union(node1, node2): 合并 node1 和 node2 所在的集合;

Representation:

并查集有两种主要的实现方式,分别是:

  1. 链式实现:

    这种实现方式理论上耗时更多,因为每次进行 union 操作的时候都要花费 O(n) 的时间改变每个 node 的parent。即使使用 weighted-union heuristic 进行优化,仍然要消耗 O(m + nlogn) 的时间(m 为操作数,n 为元素个数)。正是因为这种方法更加耗时,这里不多做介绍。

  2. Disjoint-set Forests:

    也就是传说中的树型实现方法。通过 union by rankpath compression 两种方法的优化之后,可以达到 O(m * α(n))≈O(m) (m 为操作数,n 为元素个数) 时间内完成 m 个操作的时间复杂度,也就是 O(1)。这里的 α(n) 是一个增长极其缓慢的函数,实际应用中不会超过 4。

Implementation

Disjoint-set Forests 的实现思路, 参考 wiki:Disjoint Setarrow-up-right;

这里arrow-up-right 还有一个图杀老师讲并查集的视频。

基础版本的实现(使用传统的tree node结构):

这种实现方式和CLRS或WIKI中完全一致,定义新的Node class,然后依照wiki中的伪代码实现。

QuickUnion(数组实现): 普林斯顿arrow-up-right 这种实现方式更加简短,但是需要预先设定 disjoint set 的 maxSize。

这种实现方式的本质是维持一个长度和 n 相同的 ids[] 数组,数组的 index 表示 id 本身,而其对应的 value 则表示其 parent。这样就形成了一种一一对应,环环相套的映射关系。每个元素(index)对应的 value 正是它的parent,而其parent对应到又是parent的parent,直到root,其对应的就是自己,这也是为什么一开始要将所有value都等于index;

当我们需要做path compression 的时候,只需要令 ids[x] = find(ids[x]),这一步的意义就相当于 x.parent = find(x.parent)

至于 union by rank,我们只需要另外维持一个长度和ids相等的rank数组;

Java Code:

其他:

在 Union find 的算法中还可以随时跟踪当前集合中连通块的数量,只需要在初始时候将 count 设为所有元素的总数,每次执行 union 之后令 count--;

有的人可能会觉得如果没有一一对应的从 0 开始的连续 label,就不容易应用数组表示的Union Find,其实不是的,我们可以用HashMap人为为每个Node制定id;