并查集入门
并查集的结构:
并查集用树型(非二叉树)结构实现,每个元素对应一个节点,每个组对应一棵树。
并查集是一种不相交集(Disjoint Sets)ADT,常常在使用中以森林来表示,用来管理元素的分组情况。,进行快速规整。可以以高效的进行:
1)Find,查询元素a和b是否属于同一个组
当且仅当两个元素处于同一个集合中时,Find返回相同的名字,即本操作可转换为寻找两个元素的最久远祖先是否相同,即当且仅当a和b在同一个集合中Find(a)==Find(b)。可以采用递归实现沿着树向上走来找到两棵树的根进行比较。
2)Union,合并元素a和b所在的组
设置一个数组Father[x],表示x的“父亲”的编号。那么,合并两个不相交集合的方法就是,找到其中一个树形集合的根节点,将另外一个树形集合的根节点指向它。
并查集的优化方法:
1)路径压缩
寻找根节点时,我们一般采用递归查找,但是当元素很多亦或是整棵树变为一条链时,每次Find操作都是O(n)的复杂度。通过路径压缩可以使得并查集变得更加高效,对于每个查询的甚至是经过的节点节点,一旦经过Find找到根节点后,就直接把连向父亲的边直接改为连向根节点,这样以后再次Find时复杂度就变成O(1)了。
2)按秩合并
在树形数据结构中,如果发生了退化的情况,那么并查集的复杂度便会变得很高,为了防止退化的发生,在合并之前记录下每棵树的高度(Rank),合并时如果Rank不同由Rank小的向Rank大的连边。
该算法是动态的(dynamic),因为在算法执行的过程中集合可以通过Union运算发生改变
该算法是在线的(on-line),执行Find时,必须给出答案才能继续进行
常见的应用:求无向图的连通分量个数,最近公共祖先(LCA),实现Kruskal算法求最小生成树等。
代码实现:
int father[MAX_N]; int Rank[MAX_N]; void init() { for(int i=0;i<n;i++) { father[i]=i; Rank[i]=0; } } bool same(int x,int y) { return Find(x)==Find(y); } int Find(int x) { if(father[x]==x) return x; else return father[x]=Find(father[x]); } void Unionset(int x,int y) { x=Find(x);y=Find(y); if(x==y) return; if(Rank[x]<Rank[y]) father[x]=y; else father[y]=x; if(Rank[x]==Rank[y]) Rank[x]++; }
时间复杂度:
O(n*α(n))
其中α(x),对于x=宇宙中原子数之和,α是阿柯曼函数的一个反函数,增长极慢,在可以想象的n的范围内α(x)不超过5,故可视为常数。事实上,路经压缩后的并查集的复杂度是一个很小的常数。
在Tarjan的文章里证明了这个复杂度,感兴趣的可以看一下