现在,我是用一个初学者的眼光来写并查集,此文最初写于我学并查集的那天,后经过多次修改。
1:并查集是什么
并查集是一个具有多个连通分支的图,他拥有合并两个连通分支,和查询两个元素是否位于同一个连通分支的功能。
2:并查集的简单应用
并查集解决什么问题:假如有一些点,你知道哪些点是直接相连的,但实际上间接相连也是相连,要你求有几个连通分支(即分为几块),以及任意俩个点是否位于同一个连通分支。
3:并查集在现实中的解释
我举个例子。假如有一个村,你知道其中一些人有亲属关系,求最多有几个家族。比如你知道你和你爸爸有关系,又知到你爸爸和你姑姑有关系,又知道你姑姑和你妈妈有关系,那你们是不是就是一个家族,虽然没有直接说你和妈妈有关系,但通过相连可以确定。而并查集就是查你分为几个家族,以及是否同时一个家族。
4:并查集的实现及代码
如何实现并查集?你是不是想直接用二维数组保存各元素之间的关系,那样很麻烦的,用遍历算法的时间复杂度太大了。那怎么办,你们之间可以形成一个树形结构,只要根节点相同就可以了就证明在同一个连通分支中。 就是说比如你们家族的代言人为你妈妈,然后看你和一个人的关系就看你们的代言人是不是同一个。当然要分层。
设立一个数组int pre[50010],这个数组记录了他的父亲节点是谁。如pre[100]=10代表了100的父节点是10,**当一个数的父节点是它自己的时候,证明了这个数就是根节点。**当两个元素的根节点相同时就是位于同一个集合中。
下面这个就是查找根节点的函数
int Find(int x) //寻找x的根节点
{
int r=x;
while(r!=pre[r]){ //当某个节点的父节点是自己时,即为根节点
r=pre[r]; //如果不是根节点,就让自己变为自己的父节点(从低层到高层)
}
return r; //只有当最终r==pre[r]时,即r为原来x的根节点时才会终止上面的循环,最后返回根节点
}
那实际上,怎么合并一块一块呢?假如有一个a与b相连,怎么表示他们的关系呢?实际上,就是令其中一个的根节点的父节点变为另外一个的根节点就好了。
void Join(int x,int y) //合并x和y,只要让其中一个的根节点变为另外一个根节点的父节点即可
{
int fx=Find(x),fy=Find(y); //此时的fx即为x的根节点,fy即为y的根节点
if(fx!=fy) //当两个根节点不一样时,其中一个的根节点变为另外一个根节点的父节点
pre[fx]=fy;
}
这样合并会有什么缺陷呢?在find操作中,实际的操作步数为x到其根节点的路径长度+1(边权为1),所以对于合并的树来说,我们渴望节点的平均深度尽量小,这样平均查找的次数就比较少,我们下面讲一种相对简单的优化方法,更优的方法在我后续的文章中会讲到。
初始化
(开始的时候,因为没给哪些点是相连的,所以所有点的父节点都是自己,即所有点都是根节点)
int pre[max]; //集合index的类别,或者用parent表示
int rank[max]; //集合index的层次,通常初始化为0
int data[max]; //计划index的数据结构类型
//初始化集合
void Make_pre(int i)
{
pre[i]=i; //一个集合的pre都是这个集合自己的标号。没有跟它同类的集合,那么这个集合的源头只能是自己了。
rank[i]=1; //树的高度,这个主要是用来作为合并依据的,后面来讲。
}
查找父节点
int Find(int x)
{
int r=x;
while(pre[r]!=r){ //当父节点是自己时就是根节点
r=pre[r]; //一直往上找
}
return r;
}
接下来是合并,怎么合并更好。我们前面不是设立了一个rank吗,他代表了以i为根节点的这棵树的高度。而合并的方法就是让高度较低的树的根节点的父节点变为高度较高树的根节点。
void Union(int i,int j){
int fi=Find(i); //fi为i的根节点
int fj=Find(j); //fj为j的根节点
if(fi==fj)return ; //根节点相同不做处理
if(rank[fi]>rank[fj])pre[fj]=fi; //fi高度更高,令fj的父节点为fi
else
{
if(rank[fi]==rank[fj])rank[fj]++; //相同高度的时候令fj为根节点,则以fj为根的树高度加1,如果fj更高则仅执行下面那步
pre[fi]=fj;
}
}
想要例题?先学并查集的路径压缩。很简单的,并且以后用到的并查集大多都是要路径压缩的。