我们知道,在并查集中为了减少时间复杂度,我们引入了路径压缩,路径压缩的原理说白了就是在find函数回来的路上顺便把本节点指向根节点。然而路径压缩也是有效率的,这个效率取决于本节点到根节点的路径长度。有没有一种方法可以更好的节省路径压缩的时间呢?有!这种方法就是启发式合并。启发式合并可以根据树的大小进行合并,也可以根据树的深度进行合并,但树的深度合并用处不大,主要是为了后期的可持久化算法,所以在这里介绍根据大小进行启发式合并。
我们引入这道题来介绍启发式合并。
AC代码:
#include<bits/stdc++.h>
using namespace std;
int n,m,p,x,y;
int size[5002];
int father[5002];
int find(int _x){
if(father[_x]!=_x)father[_x]=find(father[_x]);
return father[_x];
}
inline void union(int a,int b){
a=find(a);b=find(b);
if(a==b){
return;
}
if(size[a]>size[b]){
father[b]=a;
size[a]+=size[b];
}else{
father[a]=b;
size[b]+=size[a];
}
return;
}
int main(){
scanf("%d %d %d",&n,&m,&p);
for(int i=1;i<=n;i++){
father[i]=i;
size[i]=1;
}
for(int i=1;i<=m;i++){
scanf("%d %d",&x,&y);
union(x,y);
}
for(int i=1;i<=p;i++){
scanf("%d %d",&x,&y);
int r1=find(x);
int r2=find(y);
if(r1==r2)printf("Yes\n");
else printf("No\n");
}
return 0;
}
观察代码便可发现,启发式合并仅仅是增加了一个size数组,具体怎么实现,让我们先看看初始化部分:
for(int i=1;i<=n;i++){
father[i]=i;//指向自己,这一句不用说
size[i]=1;//size[i]表示以节点为根的树的大小
}
初始化时,所有点都是自成集合的,根节点为本身,所以size自然都设置为1;
inline void union(int a,int b){
a=find(a);b=find(b);
if(a==b){//如果a与b的根节点相同,那么就不进行联通,实际上这句用处不大,因为很多题目都保证给定两点不连通
return;
}
if(size[a]>size[b]){//将树的大小小的接到大的上
father[b]=a;
size[a]+=size[b];//更新根节点树大小
}else{
father[a]=b;
size[b]+=size[a];
}
return;
}
重点在于union函数,size数组在这里发挥作用:每次合并时,我们都将小树接到大树的根上,这样,造成的效果就是:减少了路径压缩的长度,提高了代码的运行效率。合并树之后别忘了更新根节点的size值!
总结:并查集是一种用途广泛且码量很少的算法,对我个人而言很喜欢这种算法,而且他与其他算法配合也能发挥出很大的威力。对于很多卡常的题目,加入启发式合并能更好的节省时间。