并查集
基本使用请参考oi-wiki
常规
一个不错的封装好的并查集写法
class UnionFind{
public int[] fa;
public UnionFind(int n){
fa = new int[n];
for(int i=0;i<n;i++)fa[i]=i;
}
public int find(int x){
if(fa[x]==x)return x;
fa[x]=find(fa[x]);//路径压缩
return fa[x];
}
public boolean union(int x,int y){
int xx=find(x),yy=find(y);
if(xx==yy)return false;//出现了环,忽略操作,返回false
fa[yy]=xx;
return true;
}
}
关于路径压缩和按秩合并
在并查集的拼接过程中,集合树的深度会不断向下生长,如果放任自流,在执行find操作时就会很耗时间(因为需要递归好多次才能找到根节点),所以关键在于减小集合树的深度。于是路径压缩与按秩合并应运而生。
所谓路径压缩,就是在find过程中顺便将很深的节点从树上取下来,直接连到根节点下,从而有效减少树的深度。虽然破坏了树的初始结构,但却没有破坏集合结构(并查集的关注点在于集合),并且有效的减少了查找时间复杂度。
并且它的实现十分简单,正如上面代码的find函数。仅仅增加了一行代码
fa[x]=find(x);//路径压缩
至于按秩合并,则是在union过程中做的优化,假设要合并a,b。我们将有两种操作,一种是将a的根节点连接到b的根节点下,另一种就是反过来,那么哪种方式会更有利于减小合并操作后的深度呢。
显然 将深度较小的集合连接到深度大的集合的根节点上更有利于减小深度:此时的深度为max(little+1,big);而如果反过来,深度为max(big+1,little)=big+1。
这就是按秩合并的思想,秩指的就是集合的深度。
具体实现较为复杂,因为需要求出或记录集合树的深度。这里不再讲述,如果有兴趣请参看oi-wiki。
事实上,只写路径压缩已经足够用了,使用路径压缩后,单次操作时间复杂度最坏情况下为o(nlogn),但平均复杂度接近于o(α(n)),其中α(n)是阿克曼函数的反函数,增长极为缓慢,在int范围下可以视为不超过10的常数(这个数字我还是往大了说的),也就是说单次操作的平均时间复杂可以看作o(1)。
并且路径压缩实现极其简单,简单到只多写了一行代码,却依然有如此优秀的时间复杂度,你还有什么理由去写按秩合并呢?
并查集可以用来解决的问题。
集合的合并与查询。
基础用法无需多说
还可以用来简单的判断无向图中是否出现环
简单来说,就是添加一条边时,发现该点已经归属于该集合了,此时就出现了环。
ps:注意,适用于无向图,对于有向图还是用拓扑排序或深度优先搜索来判环吧。
可以求图中连通分量
通过遍历fa数组,找出根节点的个数,可得连通分量的个数
也可在遍历过程中记录归属于每个集合的节点数,即每个连通分量各自的节点数
可以通过krustra算法求最小生成树或最小生成森林。
krusta算法,是逐个将最小边添加进新图中,但若该边破坏了新图的树性质(即出现了环)就舍弃此边。
而判环正是并查集的拿手好戏。因此并查集常被用于krustra算法来求最小生成树或最小生成森林。
并查集的拓展
带权并查集
参看oi-wiki