并查集
并查集是由一组互不相交的集合组成的一个集合结构。
并查集有两个基本运算:
合并 :合并两个元素所在的集合
查询 :查询两个元素是否在相同集合内
合并
两个集合中有相交时,将两个集合代表的树合并
合并实现方法(函数union1):把一棵树的根节点连在另一棵树的根节点下面合成一棵新树
void union1(int x,int y)
{
x=find(x);//x赋值为合并前x所在树的根节点
y=find(y);//y赋值为合并前y所在树的根节点
if(x!=y)s[x]=s[y];
//如果两棵树的根节点是同一个(x==y)则两个元素本就属于同一集合,无需操作
//如果两棵树的根节点不是同一个(x!=y)则合并:把根节点x连接到根节点y下面
}
这里x,y谁做新树的根节点不影响输出结果
注意用 union 定义函数会有命名冲突
查询
使用函数 find() 寻找节点的根节点
s[x]:储存该节点x的父节点
int find(int x)
{
if(x!=s[x]) return find(s[x]);//如果不是根节点,则继续查询该节点的父节点直到该节点为根节点 返回根节点
return x;//该节点为根节点 返回自身
}
通过询问它们的根节点是否为同一个来查询它们是否在相同集合内。
路径压缩
上述find()的时间复杂度为O(h),h为树的深度,那么当查询次数过多或树的深度太大时效率必然低下。实现路径压缩可以将时间复杂度变为O(logh):
int find(int x)
{
if(x!=s[x])s[x]=find(s[x]);//如果不是根节点 递归查询根节点并将其直接作为该节点的父节点
return s[x];
}
优化后顺着节点x自下而上查询(递归)过程中路过的节点的父节点都直接变成了根节点,
后续再次查询他们时间复杂度就只需要O(1)
例如图中查询节点5后,树从左图变成了右图
当再查询节点3时,就不需要再经过节点7了,3-7-1
“压缩”为3-1
随着一次次查询,树的结构越来越简单,边越来越少,时间复杂度也跟着降低。
但路径压缩后会破坏树的结构,不便于记录附加关系(如祖先后代关系等)
例题:PTA7-2 冰岛家谱
冰岛是作为一个人口稀少的国家,人群之间具有复杂的血缘关系,为了避免不必要的意外,他们的手机上都安装了一款可以随时查询两个人之间是否有血缘关系的软件。现在你的任务就是实现这样一个功能,接收血缘关系的登记信息,并在我们查询时给出两个人是否具有血缘关系。
血缘关系具有自反性、传递性。
输入格式:
一行整数n,以下n行,每行3个正整数q、a、b
若q为1,则登记a与b具有血缘关系
若q为2,则查询a与b的血缘关系,若有,输出一行YES;若无,输出一行NO。
输出格式:
对于每个q=2,输出一行YES或NO。
输入样例:
5
1 1 2
1 3 4
2 1 3
1 2 3
2 1 4
输出样例:
NO
YES
数据范围:
对于20%的数据,n<=1000
对于100%的数据,n<=10000000 ,1<=a,b<=n
代码如下:
#include<iostream>
using namespace std;
int n,i,a,b,t,f,s[1100000];
int find(int x)
{
if(x!=s[x])s[x]=find(s[x]);//路径压缩
return s[x];
}
void union1(int x,int y)
{
x=find(x);
y=find(y);
if(x!=y)s[x]=s[y];
}
int main()
{
ios::sync_with_stdio(false);cout.tie(0);cin.tie(0);
cin>>n;t=n;
for(i=1;i<=n;i++)s[i]=i;//开始时每个节点都是独立的,自己是自己的根节点
while(t--)
{
cin>>f>>a>>b;
if(f==1){union1(a,b);}
if(f==2){if(find(a)==find(b))cout<<"YES"<<endl;else cout<<"NO"<<endl;}
}
return 0;
}
启发式合并
每次合并后谁做新树的根节点并不影响结果,但会对查询的效率产生影响
图中两树合并时谁做新树根节点对查询效率显然有影响。
于是可以通过启发式合并(根据树的高度或节点的数量来决定谁做子树)
按节点数合并
把“轻”的树贴到“重”的树上。
void union2(int *h,int x,int y)
{
int r1=find(h,x);
int r2=find(h,y);
if(r1==r2)
return;
if(h[r1]>h[r2])
{
h[r2]+=h[r1];
h[r1]=r2;
}
else
{
h[r1]+=h[r2];
h[r2]=r1;
}
}
按秩合并:
根据树的高度决定谁做子树
void union3(int x,int y){
int rx=find(x);
int ry=find(y);
if(rx!=ry){
if(rk[rx]<rk[ry])s[rx]=ry;
else if(rk[ry]<rk[rx])s[ry]=rx;
else s[rx]=ry,rk[ry]++;
}
else return 0;
return 1;
}
路径压缩与启发式合并实现任意一个时间复杂度变为O(logn)
实现两个复杂度变为O(α(n)),α(n)是ackermann函数的反函数,非常小。