【初始化】
并查集在最开始时,所有的元素各自单独构成一个集合。
当集合中只有一元素时,这个集合的代表结点即为该元素,即该元素的父亲(father)是其自身。
因此并查集的初始化即将每个元素以自己作为自己的根结点。
int n;
int father[N];
void init(){
for(int i=1;i<=n;i++)
father[i]=i;
}
【查询根结点编号】
查询根结点编号是并查集中最基础的用法,即我们给出一个元素编号 x 时,能通过 Find(x) 操作寻找到元素 x 的父结点编号。
1.基本实现
我们知道,一个根结点的父结点编号是其自身,即:father[root]=root,借助这个性质,我们即可找到任意一个结点 x 的根结点。
如下图,我们知道 father[1]=1,father[2]=1,father[3]=1,father[4]=2,father[5]=2,father[6]=3,假设我们要找 6 号结点的根结点,那么过程为:首先找到 6 号点的父结点为 3 号,然后再找 3 号的父结点为 1 号,再找 1 号的父结点为 1 号,此时发现 1 号点父结点是其自身,即满足 father[root]=root,说明找到了 6 号点的根结点,即 1 号点
因此,我们利用 while 循环,即可非递归地实现 Find(x) 操作
void Find(int x){//非递归实现
while(father[x]!=x)//未查询到根结点时
x=father[x];//将当前结点更新为其根结点,继续向上寻找
return x;
}
由于并查集中每个集合都是一棵树,那么进一步,我们借助树可以利用递归来完成上述操作
void Find(int x){//递归实现
if(father[x]!=x)//未查询到根结点时
return Find(father[x]);//以当前结点的父结点进一步查询
return father[x];
}
2.路径压缩
可以发现,我们寻找 x 的父结点的过程中,不停的通过 father[x] 数组向上去寻找它的父结点,在这个过程中,相当于把这个结点到其根结点的这条路径上的所有结点都遍历了一遍,这无疑加大了时间复杂度。
为解决上述问题,就有了并查集结构中最重要的优化——路径压缩,经过路径压缩后,使得再次查询这条路径上结点的根结点时,只要 O(1) 的时间复杂度即可得到。
路径压缩的本质是减少树的层数,对于一棵集合树来说,其根结点下依附着诸多结点,在 Find(x) 的过程中,从底向下,我们递归的对结点进行更新,如果某个结点不是根结点的话,我们就将这个结点的父结点设为其父结点的父结点,逐层的进行递归,尽最大可能的去减少树的层数。
int Find(int x) {
if (x != father[x])
return father[x] = Find(father[x]);
return father[x];
}
举例来说,假设初始并查集如下:father[1]=1,father[2]=1,father[3]=2,father[4]=3,father[5]=4,现在我们要进行 Find(4) 操作
根据上述递归的路径压缩的过程:
可以看出,路径压缩完成后,有:father[1]=1,father[2]=1,father[3]=1,father[4]=1,father[5]=4,从 4 号点到 1 号点的这条路径被压缩了,当下一次查询,只需要经过一次询问,即可得到相应结点的根结点。
【合并两集合】
合并两集合同样是并查集中常见的操作。
对于分属两个集合中的元素 x、y,首先利用 Find() 操作,找出两个结点的根结点 fx、fy,然后判断根结点是否相同,若相同,说明两结点已经处于一个集合里,不需合并,若不同,将元素 y 的根结点 fy 指向 x 的根结点 fx 即可
void Union(int x,int y) {
int fx=Find(x);//x的根结点
int fy=Find(y);//y的根结点
if(fx!=fy)//判断fx与fy是否为一个根结点
father[fy]=fx;
}
【判断两元素是否属于同一集合】
判断两元素是否属于同一集合,只需要利用 Find() 操作找出两个元素的根结点 fx、fy,判断是否相等即可。
bool judge(int x,int y){
int fx=Find(x);
int fy=Find(y);
if(fx==fy)
return true;
return false;
}
【统计集合数目】
在初始化时,我们将每个元素以自己作为自己的根结点,因此当需要统计集合数目时,只需要统计有多少个元素的根结点是其自身即可。
int countSets(int n){
int cnt=0;
for(int i=1;i<=n;i++)
if(father[i]==i)
cnt++;
return cnt;
}
【统计每个集合中元素个数】
统计每个集合中元素的个数需要两步:首先,对于 n 个结点,我们要先寻找他们的父结点,借助桶排来统计父结点下的结点个数,然后,再统计父结点外的点,即对于每个元素,将其个数记为其父结点下的元素个数。
void countElements(){
for(int i=1;i<=n;i++){
father[i]=Find(i);//寻找每个节点的父结点
num[father[i]]++;//统计父结点下的节点个数
}
for(int i=1;i<=n;i++)//统计父节点外的点的个数
num[i]=num[father[i]];//对于每个元素,将其个数记为其父结点下的元素个数
}