参考书籍《算法竞赛入门到进阶》
并查集的经典例子有连通子图、最小生成树Kruskal算法和最近公共祖先(LCA)等
并查集操作的简单实现
1.初始化。定义数组int s[ ] 是以结点 i 为元素的并查集,在开始的时候还没有进行处理,所以每个点都属于独立的集,并以 i 的值表示他的集s[ i ],例如元素 1 的集s[ 1 ] = 1。
2.合并。加入第1个关系,例如(1,2)。在并查集s中把结点1合并到结点2,即把结点1的集改成结点2的集2。s[ 1 ] = 2;s[ 2 ] = 2。
3.合并。加入第2个关系,例如(1,3)。首先查找到1的集是2,再递归找到元素2的集是2,然后把2的集合并到元素3的集3。此时,结点1,2,3属一个集。s[ 1 ] = 2;s[ 2 ] = 3;s[ 3 ] = 3;
4.合并。加入第3个关系,例如(2,4)。结果为s[ 1 ] = 2;s[ 2 ] = 3;s[ 3 ] = 4; s[ 4 ] = 4;
5.查找。在上面的步骤中已经提到了查找,即递归。一直递归,直到元素的值和它的集相等就找到了根节点的集。
6.统计。如果s[ i ] = i,这是一个根节点,是它所在集的代表;统计根节点的数量就是集的数量。
1 const int maxn = 1050; 2 int s[maxn]; 3 //初始化 4 void inin_set() 5 { 6 for (int i = 1; i <= maxn; ++i) s[i] = i; 7 } 8 int find_set(int x) 9 { 10 return x == s[x]?x:find_set(s[x]); 11 } 12 void union_set(int x,int y) 13 { 14 x = find_set(x); 15 y = find_set(y); 16 if (x!=y) s[x] = s[y]; 17 }
优化:
1.合并的优化:在上诉合并操作中,首先要得到需要合并的元素x和元素y的根结点,然后合并这两个跟结点,但是如果把结点高度高的合并到小的上会继续增加搜索深度。如果我们在合并时把高度较小的集合并到较大的集上便能减少树的高度。代码实现时,初始化height[ i ]定义元素 i 的高度,在合并时更改。
1 const int maxn = 100005; 2 int s[maxn]; 3 int height[maxn]; 4 5 void init_set() 6 { 7 for (int i = 1; i <= maxn; ++i) 8 { 9 s[i] = i; 10 height[i]=0; 11 } 12 } 13 void union_set(int x,int y) 14 { 15 x = find_set(x); 16 y = find_set(y); 17 if (height[x] == height[y]) 18 { 19 height[x] = height[x]+1; 20 s[y] = x; 21 } 22 else 23 { 24 if (height[x]<height[y]) s[x] = y; 25 else s[y] = x; 26 } 27 }
2.查询的优化:路径压缩。在上诉查询操作中,查找元素 i 需要找到它的根节点,返回的结果也是根节点。而这条搜索路径可能很长,如果在返回时顺便把 i 所属的集改成根结点,那么下次再搜索的时候就能在O(1)的复杂度内得到结果。递归代码如下:
1 int find_set(int x) 2 { 3 if (x!=s[x]) s[x] = find_set(s[x]); 4 return s[x]; 5 }
这个方法称为路径压缩,因为在递归过程中,整个搜索路径上的元素所属的集都被改为根结点。路径压缩不仅优化下次查询,也优化了合并,因为在合并中也用到了查询。数据规模大的时候递归容易爆栈,下面使用非递归代码:
1 int find_set(int x) 2 { 3 int r = x; 4 while(s[r]!=r) r=s[r]; 5 int i = x,j; 6 while(i!=r) 7 { 8 j = s[i]; 9 s[i] = r; 10 i = j; 11 } 12 return r; 13 }