本文以计蒜客对并查集的介绍为基础,综合其他文章形成。
并查集(Merge-Find Set),也被称为不相交集合(Disjoint Set),是一种经常会用到的树型结构。一些常见的用途有求连通子图、求最小生成树的 Kruskal 算法和求最近公共祖先(Least Common Ancestors, LCA)等。常用来解决若干的不相交集合的如下几种操作:
1、合并操作,将包含 x 和 y 的集合合并为一个新的集合。
2、查询操作,计算 x 所在的集合。
3、判断两个元素是否属于同一个集合。
通常我们会用有根树来表示集合,树中的每一个结点都对应集合的一个成员,每棵树表示一个集合,如下图两棵树表示两个不相交集合。每个成员都有一条指向父结点的边,整个有根树通过这些指向父结点的边来维护。每棵树的根就是这个集合的代表,并且这个代表的父结点是它自己。
通过这样的表示方法,我们将不相交的集合转化为一个森林,也叫不相交森林,下图表示集合{a,b,c,d}和{e,f,g}构成的不相交森林,其中a,e为集合的代表。
![这里写图片描述](http://images.cnitblog.com/blog/358550/201309/12160744-00b6e27da65b4b48b903e768a97bf888.png)
通常并查集初始化操作是对每个元素都建立一个只包含该元素的集合,每个成员都是自身所在集合的代表。对上图的初始化如下:
![这里写图片描述](http://images.cnitblog.com/blog/358550/201309/12160852-98560aa5c6764feeab476938d1bcfe24.png)
在不相交森林中,并查集的查询操作,指的是查找出指定元素所在有根树的根结点是谁。我们可以通过每个指向父结点的边回溯到结点所在有根树的根,也就是对应集合的代表元素。
并查集的合并操作需要用到查询操作的结果。合并两个元素所在的集合,需要首先求出两个元素所在集合的代表元素,也就是结点所在有根树的根结点。接下来将其中一个根结点的父亲设置为另一个根结点。这样我们就把两棵有根树合并成一棵了。合并过程如下:
![这里写图片描述](http://images.cnitblog.com/blog/358550/201309/12160946-86baa213cc204627af4ad9a4cef4a97a.png)
并查集的查询操作在最坏情况发生时,每次合并对应到森林上都是一个点连到一条链的一端,这时时间复杂度为 O(n)O(n),其中 n 为总元素个数。如果每次都查询链的最底端,也就是最远离根的位置的元素时,复杂度便是 O(n)O(n) 了。
为了改善时间效率,可以通过启发式合并方法——按秩合并,秩表示树的高度上界,简单说就是将较矮的树接到较高树的树根上,从而防止树退化成一条链。另外,我们也可以通过路径压缩的方法来进一步减少均摊复杂度。同时使用这两种优化方法,可以将每次操作的时间复杂度优化至接近常数级。
路径压缩指的是在每次查找时,令查找路径上的每个节点都直接指向根节点,如下图:
![这里写图片描述](http://images.cnitblog.com/blog/358550/201309/12160922-bc21dc9e11d645519c158e4f5153e20d.png)
实现并查集的标准代码:
#include <stdio.h>
const int MAXN = 100; /*结点数目上线*/
int pa[MAXN]; /*p[x]表示x的父节点*/
int rank[MAXN]; /*rank[x]是x的高度的一个上界*/
void make_set(int x)
{/*创建一个单元集*/
pa[x] = x;
rank[x] = 0;
}
int find_set(int x)
{/*带路径压缩的查找*/
if(x != pa[x])
pa[x] = find_set(p[x]);
return pa[x];
}
/*按秩合并x,y所在的集合*/
void union_set(int x, int y)
{
x = find_set(x);
y = find_set(y);
if(rank[x] > rank[y])/*让rank比较高的作为父结点*/
pa[y] = x;
else
{
pa[x] = y;
if(rank[x] == rank[y])
rank[y]++;
}
}
参考文章:
计蒜客-数据结构-并查集;
并查集:http://www.cnblogs.com/cyjb/p/UnionFindSets.html;
并查集(不相交集合):http://www.cnblogs.com/ktyanny/archive/2009/12/09/1620043.html