以前经常遇到并查集,但一直没有总结。今天把并查集的相关知识总结下。
在计算机科学中,并查集是一种树型的数据结构,其保持着用于处理一些不相交集合(Disjoint Sets)的合并及查询问题。有一个联合-查找算法(union-find algorithm)定义了两个操作用于此数据结构:
- Find:确定元素属于哪一个子集。它可以被用来确定两个元素是否属于同一子集。
- Union:将两个子集合并成同一个集合。
因为它支持这两种操作,一个不相交集也常被称为联合-查找数据结构(union-find data structure)或合并-查找集合(merge-find set)。其他的重要方法,MakeSet,用于建立单元素集合。有了这些方法,许多经典的划分问题可以被解决。
为了更加精确的定义这些方法,需要定义如何表示集合。一种常用的策略是为每个集合选定一个固定的元素,称为代表(或者代表元),以表示整个集合。接着。Find(x)返回x所属集合的代表,而Union使用两个集合的代表作为参数。
需要注意的是,一开始我们假设元素都是分别属于一个独立的集合里的。
1.合并两个不相交集合(Union(x,y))
合并操作很简单:先设置一个数组Father[x],表示x的“父亲”的编号。那么,合并两个不相交集合的方法就是,找到其中一个集合最父亲的父亲(也就是最久远的祖先),将另外一个集合的最久远的祖先的父亲指向它。
也就是将一个集合的根节点的父指针指向另一个集合的根节点即可。
但是上面的union操作有一个明显的缺点,就是树的高度可能变得很大,以致find可能需要O(n)时间,(极端情况是树退化成一个线性表)。
(2) 判断两个元素是否属于同一集合(Find_Set(x))
本操作可转换为寻找两个元素的最久远祖先是否相同。可以采用递归实现。
优化
(1) Find_Set(x)时,路径压缩
寻找祖先时,我们一般采用递归查找,但是当元素很多亦或是整棵树变为一条链时,每次Find_Set(x)都是O(n)的复杂度。为了避免这种情况,我们需对路径进行压缩,即当我们经过”递推”找到祖先节点后,”回溯”的时候顺便将它的子孙节点都直接指向祖先,这样以后再次Find_Set(x)时复杂度就变成O(1)了,如下图所示。可见,路径压缩方便了以后的查找。
(2) Union(x,y)时,按秩合并
即合并的时候将元素少的集合合并到元素多的集合中,这样合并之后树的高度会相对较小。
(节点的秩:为了避免在union操作中,使树变成退化树,可以在树中的每一个节点存放一个非负的整数,称为节点的秩。节点的秩
等于以该节点作为子树的根时,该子树的高度,令x和y是当前森林中2颗不同树的根节点,rank(x)和rank(y)分别为这两个节点的秩。
在执行union(x,y)时,比较rank(x)和rank(y)
1.rank(x)<rank(y) 把y作为x的父亲,rank(x)和rank(y)不变。
2.rank(x)==rank(y) 把y作为x的父亲,rank(y)+1 (或者x也可以)
3.rank(x)>rank(y) 把x作为y的父亲,rank(x),rank(y)不变。
#include<iostream> using namespace std; #define MAX 80 int father[MAX];//fater[x]表示x的父节点 int rankArr[MAX];//rank[x]表示x的秩 void makeSet(int x) { father[x]=x;////根据实际情况指定的父节点可变化 rankArr[x]=0; // //根据实际情况初始化秩也有所变化 } ///* 查找x元素所在的集合,回溯时压缩路径*/ int findSet(int x) { if(x!=father[x]) { father[x]=findSet(father[x]); ////这个回溯时的压缩路径是精华 } return father[x]; } /* 按秩合并x,y所在的集合 下面的那个if else结构不是绝对的,具体根据情况变化 但是,宗旨是不变的即,按秩合并,实时更新秩。 */ void unionSet(int x,int y) { x=findSet(x); y=findSet(y); if(x==y) return ; if(rankArr[x]>rankArr[y]) { father[y]=x; } else if(rankArr[x]==rankArr[y]) { father[x]=y; rankArr[y]++; } else //rankArr[x]<rankArr[y] { father[x]=y; } } int main() { makeSet(0); father[4]=3; father[3]=2; father[2]=1; father[1]=1; findSet(4); father[7]=8; father[8]=9; father[9]=9; unionSet(1,7); for(int i=4;i>=1;i--) { cout<<i<<"--->"<<father[i]<<endl; } for(int i=7;i<=9;i++) { cout<<i<<"--->"<<father[i]<<endl; } }
注意我们用的是rankArr,而不是rank,因为是Stl里面有一个rank结构体。
main函数中,我们建立了2个集合,如图所示:
int findSet(int x) { if(x!=father[x]) { father[x]=findSet(father[x]); ////这个回溯时的压缩路径是精华 } return father[x]; }
为什么这点代码可以把所有子节点的父节点都指向根。
当我们调用
findSet(4); father[4]=findSet(3);
---father[3]=findSet(2)
---------father[2]=findSet(1)
由于1的父是自己,return 1.
于是回溯。
father[2]=1;
注意这时father【2】已经变成了1.这是关键。
father[3]=findSet(2),findSet返回值就是father[2]的值。为1.于是father[3]为1.
同理father[4]为1.
于是,2,,3,4的father都变成了1.
输出:
4--->1
3--->1
2--->1
1--->9
7--->9
8--->9
9--->9
请按任意键继续. . .
把findSet转成非递归:
int findSet(int x) { int p=x; while(p!=father[p]) p=father[p]; int q=x; int tmp; while(q!=p) { tmp=father[q]; father[q]=p; q=tmp; } return p; }
注意上面findSet有一个问题,由于路径压缩后有些节点秩改变了,可我们findSet根本没涉及到秩的操作。怎么回事?
我看了wikipedia findSet也是这么写的,也是没有涉及到秩rank的更改。一般来说,findSet后秩还是保持原来的秩,只是
合并时才更改秩。
复杂度分析
空间复杂度为O(N),建立一个集合的时间复杂度为O(1),N次合并M查找的时间复杂度为O(M Alpha(N)),这里Alpha是Ackerman函数的某个反函数,在很大的范围内(人类目前观测到的宇宙范围估算有10的80次方个原子,这小于前面所说的范围)这个函数的值可以看成是不大于4的,所以并查集的操作可以看作是线性的。具体复杂度分析过程见参考资料(3)。
应用
并查集常作为另一种复杂的数据结构或者算法的存储结构。常见的应用有:求无向图的连通分量个数,最近公共祖先(LCA),带限制的作业排序,实现Kruskar算法求最小生成树等。
参考:
并查集:http://www.nocow.cn/index.php/%E5%B9%B6%E6%9F%A5%E9%9B%86
并查集应用比较广泛。比如:M={1,4,6,8},N={2,4,5,7},我的需求就是判断{1,2}是否属于同一个集合。从并查集就很好实现了。
典型题目:
注意:两个城市之间可以有多条道路相通,也就是说
3 3
1 2
1 2
2 1
这种输入也是合法的
当N为0时,输入结束,该用例不被处理。
#include<iostream> using namespace std; const int N=1001; int father[N]; int total; int findSet(int x) { int r=x; while(r!=father[r]) r=father[r]; return r; } void join(int a,int b) { int fa=findSet(a); int fb=findSet(b); if(fa==fb) return; else { father[fa]=fb; total--;//统一了2个阵营的boss(根节点),所以需要数目-1 } } int main() { int n,m,x,y; while(scanf("%d",&n),n)//n代表城市数目 { scanf("%d",&m);//m代表街道数目 total=n-1;//初始化total,total为连接n个点最小需要n-1条边 for(int i=1;i<=n;i++)//注意这里是n father[i]=i;//初始化自己为自己的father for(int i=1;i<=m;i++) { scanf("%d%d",&x,&y); join(x,y); } printf("%d ",total);//输出在已有基础上还需要的边的数目! } }
题目中要求比较特殊的是:N为0时,输入结束,我们上面的是用
while(scanf("%d",&n),n) 逗号表达式来做的,也可以这么做:
while(scanf("%d%d",&n,&m)!=EOF) { if(n==0) break; 用break来退出。
测试:
输入 4 2
1 3
3 4
输出1.
该问题另外实现代码:
问题:朋友圈(25分)
假如已知有n个人和m对好友关系(存于数字r)。如果两个人是直接或间接的好友(好友的好友的好友...),则认为他们属于同一个朋友圈,请写程序求出这n个人里一共有多少个朋友圈。
假如:n = 5 , m = 3 , r = {{1 , 2} , {2 , 3} , {4 , 5}},表示有5个人,1和2是好友,2和3是好友,4和5是好友,则1、2、3属于一个朋友圈,4、5属于另一个朋友圈,结果为2个朋友圈。
最后请分析所写代码的时间、空间复杂度。评分会参考代码的正确性和效率。
思路:简单的并查集的应用
#include <iostream> using namespace std; const int MAX_N=1000; int N,M; int par[MAX_N],rank[MAX_N]; void initset(int n){ for(int i=1;i<=n;i++){ par[i]=i; rank[i]=0; } } int findpar(int x){ if(x==par[x]) return x; return par[x]=findpar(par[x]); } void unionset(int x,int y){ x=findpar(x); y=findpar(y); if(x==y)return ; if(rank[x]<rank[y]){ par[x]=y; } else{ par[y]=x; if(rank[x]==rank[y]) rank[x]++; } } int main(){ while(cin>>N>>M){ // N persons, M pair of friends. initset(N); for(int i=0;i<M;i++){ int x,y; cin>>x>>y; unionset(x,y); } int res=0; for(int i=1;i<=N;i++){ if(par[i]==i) res++; } cout<<res<<endl; } return 0; }
上面的代码可以这么改进,在每次union操作减1.
POJ 1182 食物链
Description
动物王国中有三类动物A,B,C,这三类动物的食物链构成了有趣的环形。A吃B, B吃C,C吃A。
现有N个动物,以1-N编号。每个动物都是A,B,C中的一种,但是我们并不知道它到底是哪一种。
有人用两种说法对这N个动物所构成的食物链关系进行描述:
第一种说法是"1 X Y",表示X和Y是同类。
第二种说法是"2 X Y",表示X吃Y。
此人对N个动物,用上述两种说法,一句接一句地说出K句话,这K句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。
1) 当前的话与前面的某些真的话冲突,就是假话;
2) 当前的话中X或Y比N大,就是假话;
3) 当前的话表示X吃X,就是假话。
你的任务是根据给定的N(1 <= N <= 50,000)和K句话(0 <= K <= 100,000),输出假话的总数。
Input
第一行是两个整数N和K,以一个空格分隔。
以下K行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中D表示说法的种类。
若D=1,则表示X和Y是同类。
若D=2,则表示X吃Y。
Output
只有一个整数,表示假话的数目。
Sample Input
100 7
1 101 1
2 1 2
2 2 3
2 3 3
1 1 3
2 3 1
1 5 5
Sample Output
3
题目告诉有3种动物,互相吃与被吃,现在告诉你m句话,其中有真有假,叫你判断假的个数(如果前面没有与当前话冲突的,即认为其为真话)
这题有几种做法,我以前的做法是每个集合(或者称为子树,说集合的编号相当于子树的根结点,一个概念)中的元素都各自分为A, B, C三类,在合并时更改根结点的种类,其他点相应更改偏移量。但这种方法公式很难推,特别是偏移量很容易计算错误。
下面来介绍一种通用且易于理解的方法:
首先,集合里的每个点我们都记录它与它这个集合(或者称为子树)的根结点的相对关系relation。0表示它与根结点为同类,1表示它吃根结点,2表示它被根结点吃。
那么判断两个点a, b的关系,我们令p = Find(a), q = Find(b),即p, q分别为a, b子树的根结点。
1. 如果p != q,说明a, b暂时没有关系,那么关于他们的判断都是正确的,然后合并这两个子树。这里是关键,如何合并两个子树使得合并后的新树能保证正确呢?这里我们规定只能p合并到q(刚才说过了,启发式合并的优化效果并不那么明显,如果我们用启发式合并,就要推出两个式子,而这个推式子是件比较累的活...所以一般我们都规定一个子树合到另一个子树)。那么合并后,p的relation肯定要改变,那么改成多少呢?这里的方法就是找规律,列出部分可能的情况,就差不多能推出式子了。这里式子为 : tree[p].relation = (tree[b].relation - tree[a].relation + 2 + d) % 3; 这里的d为判断语句中a, b的关系。还有个问题,我们是否需要遍历整个a子树并更新每个结点的状态呢?答案是不需要的,因为我们可以在Find()函数稍微修改,即结点x继承它的父亲(注意是前父亲,因为路径压缩后父亲就会改变),即它会继承到p结点的改变,所以我们不需要每个都遍历过去更新。
2. 如果p = q,说明a, b之前已经有关系了。那么我们就判断语句是否是对的,同样找规律推出式子。即if ( (tree[b].relation + d + 2) % 3 != tree[a].relation ), 那么这句话就是错误的。
3. 再对Find()函数进行些修改,即在路径压缩前纪录前父亲是谁,然后路径压缩后,更新该点的状态(通过继承前父亲的状态,这时候前父亲的状态是已经更新的)。
核心的两个函数为:
int Find(int x)
{
int temp_p;
if (tree[x].parent != x)
{
// 因为路径压缩,该结点的与根结点的关系要更新(因为前面合并时可能还没来得及更新).
temp_p = tree[x].parent;
tree[x].parent = Find(tree[x].parent);
// x与根结点的关系更新(因为根结点变了),此时的temp_p为它原来子树的根结点.
tree[x].relation = (tree[x].relation + tree[temp_p].relation) % 3;
}
return tree[x].parent;
}
void Merge(int a, int b, int p, int q, int d)
{
// 公式是找规律推出来的.
tree[p].parent = q; // 这里的下标相同,都是tree[p].
tree[p].relation = (tree[b].relation - tree[a].relation + 2 + d) % 3;
}
而这种纪录与根结点关系的方法,适用于几乎所有的并查集判断关系(至少我现在没遇到过不适用的情况…可能是自己做的还太少了…),所以向大家强烈推荐~~
搞定了食物链这题,基本POJ上大部分基础并查集题目就可以顺秒了,这里仅列个题目编号: POJ 1308 1611 1703 1988 2236 2492 2524。转自:http://www.cnblogs.com/ACShiryu/archive/2011/11/24/unionset.html
最近公共祖先LCA Tarjan
在一棵有根数T中,两个结点u和v的最近公共祖先(Least Common Ancestors)是指这样一个结点w, 它是u和v的祖先,并且在树T中具有最大深度。换种说法就是,对于有根树T的两个结点u、v,最近公共祖先 LCA(T, u, v):询问一个距离根最远的结点x,使得x同时为结点u、v的祖先。只有两种情况,上图:
“利用并查集优越的时空复杂度,我们可以实现LCA问题的O(n + Q)算法,这里Q表示询问的次数。Tarjan算法基于深度优先搜索的框架,对于新搜索到 的一个结点,首先创建由这个结点构成的集合,再对当前结点的每一个子树进行搜索,每搜索完一棵子树,则可确定子树内的LCA询问都已解决。其他的LCA询 问的结果必然在这个子树之外,这时把子树所形成的集合与当前结点的集合合并,并将当前结点设为这个集合的祖先。之后继续搜索下一棵子树,直到当前结点的所 有子树搜索完。这时把当前结点也设为已被检查过的,同时可以处理有关当前结点的LCA询问,如果有一个从当前结点到结点v的询问,且v已被检查过,则由于 进行的是深度优先搜索,当前结点与v的最近公共祖先一定还没有被检查,而这个最近公共祖先的包涵v的子树一定已经搜索过了,那么这个最近公共祖先一定是v 所在集合的祖先。”
为了解决最近公共祖先问题,通过对LCA(root[T])初始调用,来执行对T的树遍历。在遍历之前,假定每个结点都着色为WHITE。(同flag,标记false,true同理).下边是离线Tarjan算法的伪代码,说离线是因为,这个算法必须将所有的询问先记录下来,再一次性的求出每个点对的最近公共祖先。
LCA(u)
1 MAKE-SET(u)
2 ancestor[FIND-SET(u)] ← u
3 for each child v of u in T
4 do LCA(v)
5 UNION(u, v)
6 ancestor[FIND-SET(u)] ← u
7 color[u] ← BLACK
8 for each node v such that {u, v} ∈ P
9 do if color[v] = BLACK
10 then print "The least common ancestor of"
u "and" v "is" ancestor[FIND-SET(v)]
另外一篇文章:
1,并查集+dfs
对整个树进行深度优先遍历,并在遍历的过程中不断地把一些目前可能查询到的并且结果相同的节点用并查集合并.
2,分类,使每个结点都落到某个类中,到时候只要执行集合查询,就可以知道结点的LCA了。
对于一个结点u.类别有:
以u为根的子树、除类一以外的以f(u)为根的子树、除前两类以外的以f(f(u))为根的子树、除前三类以外的以f(f(f(u)))为根的子树……
类一的LCA为u,类二为f(u),类三为f(f(u)),类四为f(f(f(u)))。这样的分类看起来好像并不困难。
但关键是查询是二维的,并没有一个确定的u。接下来就是这个算法的巧妙之处了。
利用递归的LCA过程。
当lca(u)执行完毕后,以u为根的子树已经全部并为了一个集合。而一个lca的内部实际上做了的事就是对其子结点,依此调用lca.
当v1(第一个子结点)被lca,正在处理v2的时候,以v1为根的子树+u同在一个集合里,f(u)+编号比u小的u的兄弟的子树 同在一个集合里,f(f(u)) + 编号比f(u)小的 f(u)的兄弟 的子树 同在一个集合里……
而这些集合,对于v2的LCA都是不同的。因此只要查询x在哪一个集合里,就能知道LCA(v2,x)
还有一种可能,x不在任何集合里。当他是v2的儿子,v3,v4等子树或编号比u大的u的兄弟的子树(等等)时,就会发生这种情况。即还没有被处理。还没有处理过的怎么办?把一个查询(x1,x2)往查询列表里添加两次,一次添加到x1的列表里,一次添加到x2的列表里,如果在做x1的时候发现 x2已经被处理了,那就接受这个询问。(两次中必定只有一次询问被接受).
3,应用:http://acm.pku.edu.cn/JudgeOnline/problem?id=1330
实现代码:
、
#include<iostream> #include<vector> using namespace std; const int MAX=10001; int f[MAX]; int r[MAX]; int indegree[MAX];//保存每个节点的入度 int visit[MAX]; vector<int> tree[MAX],Qes[MAX]; int ancestor[MAX]; void init(int n) { for(int i=1;i<=n;i++) { r[i]=1; f[i]=i; indegree[i]=0; visit[i]=0; ancestor[i]=0; tree[i].clear(); Qes[i].clear(); } } int find(int n) { if(f[n]==n) return n; else f[n]=find(f[n]); return f[n]; }//查找函数,并压缩路径 int Union(int x,int y) { int a=find(x); int b=find(y); if(a==b) return 0; //相等的话,x向y合并 else if(r[a]<=r[b]) { f[a]=b; r[b]+=r[a]; } else { f[b]=a; r[a]+=r[b]; } return 1; }//合并函数,如果属于同一分支则返回0,成功合并返回1 void LCA(int u) { ancestor[u]=u; int size = tree[u].size(); for(int i=0;i<size;i++) { LCA(tree[u][i]); Union(u,tree[u][i]); ancestor[find(u)]=u; } visit[u]=1; size = Qes[u].size(); for(int i=0;i<size;i++) { //如果已经访问了问题节点,就可以返回结果了. if(visit[Qes[u][i]]==1) { cout<<ancestor[find(Qes[u][i])]<<endl; return; } } } int main() { int cnt; int n; cin>>cnt; while(cnt--) { cin>>n;; init(n); int s,t; for(int i=1;i<n;i++) { cin>>s>>t; tree[s].push_back(t); indegree[t]++; } //这里可以输入多组询问 cin>>s>>t; //相当于询问两次 Qes[s].push_back(t); Qes[t].push_back(s); for(int i=1;i<=n;i++) { //寻找根节点 if(indegree[i]==0) { LCA(i); break; } } } return 0; }
参考:http://kmplayer.iteye.com/blog/604518
对于这个算法的应用,很好的一个例子是(HDU 2586 How far away ?)。题意是,求在一颗无向树中,任意两点间的距离。利用的简单的公式:distance(a,b) = dis[a] + dis[b] – 2 * dis[LCA(a,b)]即可求出。
#include<iostream> #include<cstdio> #include<cstring> using namespace std; const int N = 40001; struct Edge { int v, w, next; }edge[2 * N]; int n, m, size, head[N]; int x[N], y[N], z[N], root[N], dis[N]; bool mark[N]; //插入边 void Insert(int u, int v, int w) { edge[size].v = v; edge[size].w = w; edge[size].next = head[u]; head[u] = size++ ; edge[size].v = u; edge[size].w = w; edge[size].next = head[v]; head[v] = size++ ; } //查找操作 int Find(int x){ if(root[x] != x) { return root[x] = Find(root[x]); } return root[x]; } void LCA_Tarjan(int k) { mark[k] = true; root[k] = k; //m次询问, z[i]保存的是点 x[i] 和 y[i] 最近公共祖先 for(int i = 1; i <= m; i++ ) { if(x[i] == k && mark[y[i]]) z[i] = Find(y[i]); if(y[i] == k && mark[x[i]]) z[i] = Find(x[i]); } for(int i = head[k]; i != -1; i = edge[i].next) { if(!mark[edge[i].v]) { dis[edge[i].v] = dis[k] + edge[i].w; LCA_Tarjan(edge[i].v); root[edge[i].v] = k; } } } int main() { int cas, u, v, w, i; scanf("%d", &cas); while(cas--) { scanf("%d %d", &n, &m); size = 0; memset(head, -1, sizeof(head)); for(i = 1; i < n; i++ ) { scanf("%d %d %d", &u, &v, &w); Insert(u, v, w); } for(i = 1; i <= n; i++ ) { x[i] = y[i] = z[i] = 0; } for(i = 1; i <= m; i++ ) { scanf("%d %d", &u, &v); x[i] = u; y[i] = v; } memset(mark, false, sizeof(mark)); dis[1] = 0; LCA_Tarjan(1); for(i = 1; i <= m; i++ ) { printf("%d ", dis[x[i]] + dis[y[i]] - 2 * dis[z[i]]); } } return 0; }
poj1470 Closest Common Ancestors
这道题和上面那道一样,很典型的LCA问题,不过读入有点麻烦,求的是每个点被作为最近公共祖先的次数,呵呵。。
代码 //============================================================================ // Name : poj1470.cpp // Author : birdfly // Description : 最近公共祖先 //============================================================================ #include <iostream> #include <stdio.h> #include <string.h> #define NN 902 using namespace std; typedef struct node{ int v; struct node *nxt; }NODE; NODE edg1[NN * 2], edg2[NN * 1000];//数组要开大点 NODE *Link1[NN], *Link2[NN]; int idx1, idx2, N, M; int fat[NN]; int vis[NN]; int cnt[NN]; void Init(NODE *Link[], int &idx){ memset(Link, 0, sizeof(Link[0]) * (N + 1)); idx = 0; } void Add(int u, int v, NODE edg[], NODE *Link[], int & idx){ edg[idx].v = v; edg[idx].nxt = Link[u]; Link[u] = edg + idx++; edg[idx].v = u; edg[idx].nxt = Link[v]; Link[v] = edg + idx++; } int find(int x){ if(x != fat[x]){ return fat[x] = find(fat[x]); } return x; } void Tarjan(int u){ vis[u] = 1; fat[u] = u; for (NODE *p = Link2[u]; p; p = p->nxt){ if(vis[p->v]){ cnt[find(p->v)]++; } } for (NODE *p = Link1[u]; p; p = p->nxt){ if(!vis[p->v]){ Tarjan(p->v); fat[p->v] = u; } } } int main() { int i, u, v, n, root; int flag[NN]; while(scanf("%d", &N) != EOF){ Init(Link1, idx1); memset(flag, 0, sizeof(flag)); for (i = 1; i <= N; i++){ //数据的读入方式很不错啊 scanf("%d", &u); while(getchar() != '('); scanf("%d", &n); while(getchar() != ')'); while(n--){ scanf("%d", &v); flag[v] = 1; Add(u, v, edg1, Link1, idx1); } } scanf("%d", &M); Init(Link2, idx2); for (i = 1; i <= M; i++){ while(getchar() != '('); scanf("%d%d", &u, &v); while(getchar() != ')'); Add(u, v, edg2, Link2, idx2); } memset(vis, 0, sizeof(vis)); memset(cnt, 0, sizeof(cnt)); for (i = 1; i <= N; i++){// 第一个结点不一定是根结点 if(flag[i] == 0) break; } root = i; Tarjan(root); for (i = 1; i <= N; i++){ if(cnt[i]){ printf("%d:%d ", i, cnt[i]); } } } return 0; }
参考:
http://www.slyar.com/blog/disjoint-set.html
http://www.cnblogs.com/cherish_yimi/archive/2009/10/11/1580839.html
更多:
http://www.cnblogs.com/DreamUp/archive/2010/07/19/1780916.html
http://www.cnblogs.com/ACShiryu/archive/2011/11/24/unionset.html