并查集的作用
1、将两个集合合并
2、询问两个元素是否在一个集合当中
讲一下我在学习时这里遇到的误区:集合和元素的概念
这里讲的稍微清楚一点其实就是,一个集合里包含多个数组(字符串或者数字)如下图:
如果我们已经学习了树(Trie)的知识,我们也可以用树来表示集合的概念:
基本原理
1、用树的方式来描述每一个集合;
2、每一个集合的编号就是根节点的编号;(例如上图的集合1,那么根节点的编号就是1,集合2的根节点的编号就是2)
3、 每一个节点,都用一个p[]来存储这个节点的父节点是谁;(如下图)
例如我们有这样一个树:
那我们前面说到的p[]的作用就是,假如当前节点是8,那么p[8]=4,同理p[9]=4,而p[4]=2,p[2]=1(1就是根节点,也就是p[1]=1)
假如我们想求出4是属于哪个集合的,那就可以通过p[4]=2->p[2]=1->p[1]=1来找出4是属于1这个集合的。
大家有没有发现,除了树根,都是p[x]≠x,最后找到树根时才会是p[x]=x,下面我们就要讲到几个要解决的问题:(在y总的课上这里有点难理解,在这句话中,可以暂时把x比作任意的数字,注意仅仅是这句话,最后其实x就是代表的根节点,大家需要动动脑,或者自己画个树理解一下)
问题1、如何判断树根? 解决办法:if(p[x]==x);
问题2、如何求x的集合编号?while(p[x]!=x) x=p[x];
问题3、如何合并两个集合?如下:
假设我们有两个集合:
我们只可以将第一个集合插入到第二个集合中,或者将第二个集合插入到第一个集合中
以第一个集合插入到第二个集合为例,我们只需要将第一个集合的根节点直接连接到第二个集合中的某个节点的位置,注意是某个节点位置哦,不一定是集合2的根节点
我们假设要将第一个集合连接到第二个集合的根节点的位置,第二个集合的根节点是y,第一个集合的根节点是x,那只要p[x]=y就可以了,因为一开始是p[x]=x,更具我们前面说到的p[]是当前节点的父节点位置,所以此时我们只需要将原本是父节点的p[x]当作子节点,然后指向新的父节点y即可
优化
由于问题2中,每一次都需要将当前节点遍历到根节点,因此复杂度依然有些高(和树的层数成正比),此时我们就需要对原来的算法进行一些优化。
当我们在寻找一个节点的根节点时,在树中一定时存在一条路径的,由于这条路径的根节点必然是相同的,所以可以将经过过的节点,一次性全部化为根节点的第二层节点,如图:
例题1合并集合
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=1e5+10;
int p[N];
int n,m;
int find(int x){
if(p[x]!=x) p[x]=find(p[x]);
return p[x];
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++) p[i]=i;
while(m--){
char op[2];
int x,y;
scanf("%s%d%d", op, &x, &y);
if(*op=='M'){
p[find(x)]=find(y);
}else{
if(find(x)==find(y)) cout<<"Yes"<<endl;
else cout<<"No"<<endl;
}
}
return 0;
}
例题2亲戚
#include<iostream> //本题使用cin和cout会超时
#include<algorithm>
#include<cstring>
using namespace std;
const int N=2e4+10;
int p[N];
int n,m,q;
int find(int x){
if(p[x]!=x) p[x]=find(p[x]);
return p[x];
}
int main(){
cin>>n>>m;
for(int i=1;i<=n;i++) p[i]=i;
while(m--){
int a,b;
scanf("%d %d",&a,&b);
p[find(a)]=find(b);
}
int q;
cin>>q;
while(q--){
int c,d;
scanf("%d %d",&c,&d);
if(find(c)==find(d)) printf("Yes\n");
else printf("No\n");
}
return 0;
}