并查集
大致概念
一种解决元素分组和不相交集合的合并,查询问题的数据结构。
大量的树(单个节点也算是树)经过并根生成一系列家族(即森林)的过程。每个集合即每棵树都是由唯一根节点确定,也可以理解为每个家族的族长就是根节点(递归最后一层的结点)。
如:有16个结点(树),a结点所在的树的根是a,而c的根是c,我想让ac并根,即c所在的树的根合并到a所在的树的根下,称为a树的子节点,从今以后,c的根不再是c,而是a.
同理,我让数字类,与字母类分别并根
逻辑关系如下
并查集的存储结构
并查集采用数组表示整个森林
并查集数组: int bcj[Maxn+1] 对于bcj[x]=y,有:
x是某个结点,y是这个节点所在树的根,初始化并查集数组时自己即为所在森林的树根(f[x]=x)。
// 假设初始有860个结点
# define Maxn 860
int bcj[Maxn+1];//并查集数组f[x]=y,x是某个结点,y是这个节点所在树的根
void bcj(){//并查集的初始化
for(int i =0;i<=Maxn; i++)bcj[i]=i;//当然为了简明,bcj可写为fa/root表示存的是该点的树根
}
并查集的查询
递归法:递归地访问父节点,直到访问到根结点处(因为代码实现中,访问到根结点处,查找根节点的父亲是否是自己,即查询自己是否是根节点,如果是,则返回)
对于某个并查集中的任意结点,通过查询能找到其根节点
//这种方法容易导致某个并查集树的高度很长
int find(int x){
if(fa[x] == x)return x;
else return find(fa[x]);
}
但是,如果每次都查询最后一个,那么他就要经过多次递归,非常消耗时间,这时候我们就要引入路径压缩。
路径压缩
路径压缩是为了解决当树的高度过高的时候,提高查询时效的方法。
解决方式也很简单,在递归的同时将路径压缩,那么上面的图经过一次查询后的效果如下。
int find(int x){
if(x == fa[x])return x;
else{
fa[x] = find(fa[x]); //父节点设为根节点
return fa[x]; //返回父节点
}
}
判断两个元素是否是亲属(属于同一个集合)
判断 A,B 是否属于一个集合直接判断 isBro=(find(A)== find(B)?YES:NO);。
合并(核心)
把一颗树的根节点设置为另一棵树的根节点。
还有一种方式是按秩合并,但是我们使用路径压缩时间复杂度就已经很低了,此处不做讲解。
例如:合并前
合并后:
void merge(int i, int j){
fa[find(i)] = find(j);
}
启发式合并
合并操作避免发生退化:
合并时,选择哪棵树的根节点作为新树的根节点会影响未来操作的复杂度。
我们可以按照子树大小去合并,小的合并到大的,注意是合并到!
所以启发式合并的原理是在集合合并时 将小的集合 合并到 大的集合里,也可以使 find 操作复杂度降低到 O(logn),在集合合并时还要增加一个更新集合大小的操作。
//启发式合并
void merge(int x,int y){
x=find(x);y=find(y);
if(x!=y){
if(sz[x]<sz[y])swap(x,y);//代码逻辑不管:确保第一参数是相对大的集合
sz[x]+=sz[y];//大集合扩容,即大集合空间加上小集合的大小
fa[y]=x;//并根:小集合的根为x
}
}
并查集模板
#include<bits/stdc++.h>
using namespace std;
#define MAXN 2000000
int fa[MAXN+1];//并查集采用数组表示整个森林,初始时每个森林的树根为自己。
//并查集初始化
void bcj(){
for(int i=0;i<=n;++i)fa[i]=i;
}
//查找
//int find(int x){
// if(fa[x]==x)return x;
// else return find(fa[x]);
//}
//查找+路径压缩(可以让树的高度降低为1)
int find(int x){
if(x==fa[x])return x;
else{
fa[x]=find(fa[x]);//父节点设为根节点
return fa[x];//返回父节点
}
}
//合并
void merge(int i,int j){
fa[find(i)]=find(j);
}
//启发式合并
//void merge(int x,int y){
// x=find(x);y=find(y);
// if(x!=y){
// if(sz[x]<sz[y])swap(x,y);
// sz[x]+=sz[y];
// fa[y]=x;
// }
//}
int main(){
return 0;
}
例题
例如:
5×4 的小格子,编号:
1 2 3 4 5
6 7 8 9 10
11 12 13 14 15
16 17 18 19 20
输出描述
输出植物数量。
输入输出样例
输入
5 416
2 3
1 5
5 9
4 8
7 8
9 10
10 11
11 12
10 14
12 16
14 18
17 18
15 19
19 20
9 13
13 17
输出
5
合根参考
#include<bits/stdc++.h>
using namespace std;
#define MAXN 2000000
int fa[MAXN+1];
int n,m;//n行m列
int k;//k次并根
void bcj(){
for(int i=0;i<=MAXN;++i)fa[i]=i;
}
int find(int x){//查找+路径压缩
if(x==fa[x])return x;//如果查找的元素的根就是自己,返回
else{//如果不是,递归地往上搜索根节点
fa[x]=find(fa[x]); //将根节点赋给这个元素的并查集
return fa[x];//返回这个元素的并查集(该元素的根)
}
}
void merge(int i,int j){//前者并根到后者,后者为根
fa[find(i)]=find(j);//合并,将i元素的根改为j元素的根
}
int main(){
ios::sync_with_stdio(0);cin.tie(nullptr);cout.tie(nullptr);
bcj();
cin>>n>>m>>k;
int a,b;
for(int i=1;i<=k;++i){
cin>>a>>b;
merge(a,b);//并根
}
int ans=0;
for(int i=1;i<=n*m;++i){//查询有多少个元素的根仍然是自己(即存在多少个并查集)
if(fa[i]==i)++ans;
}
cout<<ans;
return 0;
}