参考资料:《算法笔记》
并查集是一种维护集合的数据结构,他的名字取自“并”(union)、“查”(find)“集”(set)。并查集支持以下两种操作:
使用一个数组实现:int father[n];
,其中father[i]表示元素i的父结点,这个父结点本身也属于这个集合。
如果father[i] == i
,则说明元素i是这个集合的根结点,对于同一个集合只存在一个根结点,且将其作为所在集合的标识。
father[1] = 1;
father[2] = 1;
father[3] = 2;
father[4] = 2;
father[5] = 5;
father[6] = 5;
这样就得到了两个不同的集合
并查集的使用首先需要初始化father数组,然后根据需要进行查找或者合并的操作。
一开始每个元素都是各自属于一个集合,需要令所有father[i] = i
:
for(int i = 0; i <= n; i ++ )
father[i] = i;
由于同一集合只存在一个根结点,因此查找操作就是对给定结点寻找对应根结点的过程。实现方式可是递归或者递推,都是反复寻找父结点,直到找到根结点(满足father[i] == i
的结点)
递推的代码:
//findfather函数返回元素x所在集合的根结点的编号
int findfather(int x){
while(x != father[x]){ //如果不是根结点就继续循环
x = father[x]; //获得自己的父亲结点
}
return x;
}
递归的代码:
int findfather(int x){
if(x == father[x]) return x; //找到根结点就返回根结点编号
else return findfather(father[x]); //否则递归判断x的父节点是否是根结点
}
合并指的是将两个集合合并为一个集合。题目中一般给出两个元素,要求把这两个元素所在的集合合并。一般是先判断这两个元素是否属于同一集合,只有当这两个元素属于不同集合时,才合并,而且合并的过程一般是把其中一个集合的根结点的父亲设置为另一个集合的根结点。
主要分为一下两步:
findfather
函数对a、b查找根结点,再判断是否相同;father[faA] = faB;
void union(int a, int b){
faA = findfather(a);
faB = findfather(b);
if(faA != faB) father[faA] = faB;
}
注意: 并非是将father[a] = b,而是对a、b所在集合的根结点进行操作。
路径压缩就是对并查集的查询进行优化。
由findfather函数的目的就是查找根结点,下面这个例子:
father[1] = 1;
father[2] = 1;
father[3] = 2;
father[4] = 2;
如果只是为了找到根结点,可以把操作等价变换成:
father[1] = 1;
father[2] = 1;
father[3] = 1;
father[4] = 1;
相当于把查询结点的路径上的所有结点的父亲都设置为根结点,查找的时候就不必一直回溯去找父亲了,查询的复杂度降为O(1)。这个转换称为路径压缩。
原先的findfather函数是从给定结点不断或得它的父亲最终找到根结点,转换的过程可以分为以下两个步骤:
int findfather(int x){
//由于x在下面的while中会变成根结点,把原先的x保存一下
int a = x;
while(x != father[x]) x = father[x]; //寻找根结点
//while结束后 x存放的是根结点。下面把路径上的所有结点的father都改成根结点
while(a != father[a]){
int z = a; //因为a会被father[a]覆盖,先存一下a的值,以修改father[a]
a = father[a]; //a回溯到父结点
father[z] = x; //将原先的结点a的父亲改为根结点x
}
return x; //返回根结点
}
这样就可以在查找的时候把寻找根结点的路径压缩了,可以把路径压缩后的并查集查找函数均摊效率认为是O(1)。
路径压缩的递归写法:
int findfather(int x){
if(x == father[x]) return x; //找到根结点
else{
int F = findfather(father[x]); //递归寻找x的父亲的根结点
father[x] = F; //将根结点赋值给father[x]
return F; //返回根结点F
}
}
AcWing 836. 合并集合
注意:需要路径压缩,否则会超时
代码:
//AcWing 836. 合并集合
#include
using namespace std;
const int maxn = 1e5 + 10;
int n, m; //n个数 m个操作
int father[maxn]; //父结点
int findfather(int x){
if(x == father[x]) return x; //找到根结点
else{
int F = findfather(father[x]); //递归寻找x的父亲的根结点
father[x] = F; //将根结点赋值给father[x]
return F; //返回根结点F
}
}
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++ ) father[i] = i; //初始化father[]数组
for(int i = 0; i < m; i ++ ){
char op;
int a, b;
cin >> op >> a >> b;
if(op == 'M'){ //合并
int fa = findfather(a);
int fb = findfather(b);
if(fa != fb) father[fa] = fb;
}
if(op == 'Q'){
if(findfather(a) == findfather(b)) puts("Yes");
else puts("No");
}
}
return 0;
}