并查集
相关概念
1. 不相交集
将编号分别为1…N的N个对象划分为不相交集合,在每个集合中选择其中某个元素代表所在集合。
2. 并查集
并查集是一种树型的数据结构,用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。有一个联合-查找算法(union-find algorithm)定义了两个用于此数据结构的操作:
- Make_Set(x) 把每一个元素初始化为一个集合。
- Find(x):确定元素x属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。
- Union(a,b):将a,b两个元素所在的子集合并成同一个集合。
由于支持这两种操作,一个不相交集也常被称为联合-查找数据结构(union-find data structure)或合并-查找集合(merge-find set)。其他的重要方法,MakeSet,用于建立单元素集合。有了这些方法,许多经典的划分问题可以被解决。
3. 相关操作
Make_Set(x)
初始化后每一个元素的父亲节点是它本身,每一个元素的祖先节点也是它本身,也可以根据具体情况来进行决定,比如有同学提到使用memset对所有元素的初始值赋值为非法值-1,对数据集较大时相对于循环赋值有一定的优化。
Find(x)
查找一个元素所在的集合,其精髓是找到这个元素所在集合的祖先,该功能有find完成。判断两个元素是否属于同一集合,只要看他们所在集合的祖先是否相同即可。
Union(a,b)
合并两个不相交集合操作很简单:利用Find_Set找到其中两个集合的祖先,将一个集合的祖先指向另一个集合的祖先。如图 一 1所示,当合并e和h时,需要将e和h所在的两个集合进行合并,即将一个节点的父节点作为另一个集合中父节点的儿子,构成一个集合。
图 一 1 Union前后结构变化
相关算法
1. 数组实现
- Make_Set(x)
用数组记录每个元素所属的集合编号,所以在初始化的时候可以直接将所有的数组元素记录为对应的下标,表示该元素自己在一个不相交集合中。由于需要初始化每一个元素,所以复杂度为O(n)。 - Find(x)
查找元素所属的集合时,只需读出数组中记录的该元素所属集合的编号即为该元素所在的集合,所以复杂度为O(1)。 - Union(a,b)
合并两元素各自所属的集合时,需要将数组中属于其中一个集合的元素所对应的数组元素值全部改为另一个集合的编号值。由于每次需要修改一个集合的元素,所以每次合并的复杂度为O(k),k为集合的大小,一种优化方式是每次修改较小的集合,这虽然有一定的加速,但是并没有修改最差情况。
2. 链表实现
- Make_Set(x)
用链表表头代表每个元素所属的集合编号,所以在初始化的时候可以直接将所有的指向自己所对应的表头,表示该元素自己在一个不相交集合中。由于需要初始化每一个元素,所以复杂度为O(n)。 - Find(x)
查找元素所属的集合时,需要判断其在哪一个链表当中,需要进行遍历,所以复杂度为O(n)。 - Union(a,b)
合并两元素各自所属的集合时,需要将其中的一个链表连接到另一个链表。虽然连接两个链表只需要O(1)的时间,但是需要确定两个元素所在的集合,所以该算法复杂度仍为O(n)。
3.树实现
- Make_Set(x)
用有根树来表示集合,树中的每个节点包含集合的一个成员,每棵树表示一个集合。多个集合形成森林态,以每棵树的树根作为集合的代表,并且根结点的父结点指向其自身,树上的其他结点都用一个父指针表示它的附属关系。树的指针起的只是联系集合中元素的作用,该思想与链表类似。由于需要初始化每一个元素,所以复杂度为O(n)。 - Find(x)
查找元素所属的集合时,需要判断其在哪一棵树当中,需要进行遍历,但是理想情况下如果是BST,则复杂度为O(logn)。 - Union(a,b)
合并两元素各自所属的树时,需要将其中的一棵树连接到另一棵树。合并两颗BST需要O(logn),查找所在的集合也需要O(logn),所以总的复杂度仍为O(logn)。
4. 路径压缩
考虑到大量的find操作,每一次都需要从跟向上遍历的过程,如果多次查找,是否可以利用之前的查找过程,来对后面的查找进行加速,基于此思想,提出了路径压缩。如图 一 2所示,每次查找一个元素的过程中,将该元素到根节点的所有元素连接到根节点,这样虽然第一次查找复杂度较高,但是后面可以直接得到结果,经证明该方法的摊还时间复杂度O(α(n)),其中α为阿克曼函数的反函数,该函数基本接近于常数时间复杂度。
图 一 2 路径压缩效果图
5. 具体实现
使用数组表示该节点所在的集合,例如对于数组father[n],如果father[i]=i则说明i就是i所在集合的代表,如果father[i] !=i,说明i和father[i]属于同一集合。伪代码如下:
Make_Set(n)
maker_set(n)
int father[n]
for i = 0 to n – 1
father[i] = i
Find(x)
find(x)
f = father[x]
if(f != x)
father[x] = find(f)//路径压缩
return father[x]
Union(a,b)
union(a,b)
union(a,b)
fa = find(a)
fb = find(b)
father[fb] = fa
6. 带权并查集
使用一个额外的数组来表示同一颗子树之间的关系,一般需要区别同一类别中的元素时使用。
并查集应用
POJ1182 食物链
POJ1733 Parity game
POJ1417 True Liars
POJ2912 Rochambeau