并查集
并查集是一种很实用的算法,那么就让我们讲一讲并查集吧!
并查集的定义
1. 并查集是一种树型的数据结构,用于处理一些不相交集合的合并及查询问题。
2. 并查集通常包含两种操作
1)查找(Find):查询两个元素是否在同一个集合中
2)合并(Union):简称uni,就是把两个不相交的集合合并为一个集合
我们来做一道很好的练习题——亲戚。
这道题其实我们可以用搜索,强力搜出两者是否有亲戚关系,可是这也太麻烦了,有没有什么更好的办法呢?
没错,就是并查集。
并查集其实就是一个模板,首先我们来看看find函数。
int find(int x){
if(f[x]==x){
return x;
}else{
return f[x]=find(f[x]);
}
}
其中数组f代表了一个人的父节点,如果一个人的父节点是他自己,那么他就到终点了。
如果他还有父亲,那么我们就继续找他的父亲。
那么uni合并函数又是怎样的呢?更简单了:
void uni(int x,int y){
f[find(y)]=find(x);
}
就是将两个人的父节点变成一个。也就是说,我们找到了父节点,就找到了并查集的根本所在。
其实并查集就是将两个不同的树合并成一个,第一次合并,可能只有两个节点,可到后来就需要两个大树合并成一个超大树,这时我们只需要将其中一棵树的根节点指向另一棵树的根节点,也就是将一棵树放进另一棵树当中,就这么简单!
find
我们接下来先详细解释一下find函数什么意思,请看如下代码:
f[x]=find(f[x]);
可能你看这段代码还不理解,那我们就拿一道题举例:亲戚。
在题目中,我们每个亲戚的关系可以用搜索做,但是这样太麻烦了,我们就要用到并查集中的靠左法则。代码中的x是某一个亲戚,而我们知道,如果一个人的祖先是自己,那他就是最“老”的祖先(根节点),并查集就是利用这种树的关系。
f[x]就是代表一个人的祖先是谁,可是如果我们继续找祖先是否有祖先,发现这个祖先竟然还有祖先,那是不是说,x的祖先另有其人?我们f[x]里要存储的是一个人最“老”的祖先!这段代码是干什么的了呢?当我们要调查一个人的祖先时,就要调用find函数,如果发现它的祖先是自己,那么我们是不是就知道它就是最老的祖先了呢?这时候我们就返回他自己是最老的祖先。如果他的祖先不是自己,也就是他不是最老的祖先,那我们就找他的祖先是否有祖先,如果它的祖先没有祖先,也就是它的祖先是最老的,那么就停止。你发现了没有,这其实就是一个递归的过程,我们修改祖先也就是在回溯的时候,不影响时间复杂度,这时你就知道了吧,find函数就是用这样的原理来寻找并修改自己的祖先的。
union
uni函数包含了两个法则:从根修改法则和靠左法则。
首先我们来看看uni函数的核心代码:
f[find(y)]=find(x);
find函数你已经知道是什么意思了,我们就来看看这是怎么合并的。
如果要将两个结点合并,其实比较简单,就是将一个结点的父节点指向另一个节点,两个树合并,就是要将他们的根结点如上合并,也很简单。我们知道了并查集其实就像一棵树。如果我们将给入的两个节点合并,那可能会使树很深,最后搜索答案麻烦。可是如果我们找到他们的根结点,将其中一棵树的根结点的父亲指向另一个根结点,相当于使一个树的根结点的子树增加,这样的话深度不变,搜索起来很方便,所以我们就要找到两者的最老祖先合并,也就是如上需要调用find函数的原因,而这一步就是从根修改法则的根本。
那靠左法则什么意思呢?更简单,就是将y的最老祖先“认爹”,而这个新的父节点就是x的最老祖先,也就是说认爹祖先在左,父节点在右。
这就是find和uni的主要用途。那我们就拿亲戚这一题举例,看懂代码你就看懂了并查集。
#include<bits/stdc++.h>
using namespace std;
int f[5001];
int fd(int x){//find
if(f[x]==x){
return x;
}else{
return f[x]=fd(f[x]);
}
}
int main(){
int n,m,p;
cin>>n>>m>>p;
for(int i=1;i<=n;i++){
f[i]=i;
}
for(int i=1;i<=m;i++){
int x,y;
cin>>x>>y;
f[fd(y)]=fd(x);//uni
}
for(int i=1;i<=p;i++){
int x,y;
cin>>x>>y;
if(fd(x)==fd(y)){//如果祖先相同
cout<<"Yes"<<endl;
}else{
cout<<"No"<<endl;
}
}
return 0;
}
是不是很直白明了!很简单吧!这就是神奇好玩的并查集!!
By ImNot6Dora