并查集学习
并查集用于解决元素分组的问题,管理一系列不相交的集合,并支持两种操作:
- 合并:把两个不相交的集合合并一个集合
- 查询:查询两个元素是否在同一个集合中
参考:
详细分析
代码模板:
最简单的版本(易理解但效率较低)
初始化:
int fa[MAXN];
void init(int n)
{
for(int i=1;i<=n;i++)
{
fa[i]=i;
}
}
查询:
一层一层访问父节点,直至根节点(根节点的标志就是父节点是本身)。要判断两个元素是否属于同一个集合,只需要看它们的根节点是否相同即可。
int find(int x)
{
if(fa[x]==x)
{
return x;
}
else
return find(fa[x]);
}
合并:
合并操作先找到两个集合的根元素,然后将前者的父节点设为后者,也可以将后者的父节点设为前者。
void merge(int i,int j)
{
fa[find(i)] = find(j);
}
存在问题:最简单的并查集效率是比较低的。
路径压缩版(优化)
路径压缩解决效率问题。查询的过程中,把沿途的每个节点的父节点都设为根节点。
int find(int x)
{
if(x==fa[x]) return x;
else
{
fa[x] = find(fa(x));//将父节点设为根节点
return fa[x]; //返回父节点
}
}
进一步优化——按秩合并
路径压缩和按秩合并如果一起使用,时间复杂度接近 O(n);
思路:
用一个数组rank[]
记录每个根节点对应的树的深度(如果不是根节点,其rank相当于以它作为根节点的子树的深度)。
最开始,把所有元素的rank(秩)设为1。
合并时比较两个根节点,把rank较小者往较大者上合并。
初始化(按秩合并)
void init(int n)
{
for(int i=0;i<n;i++)
{
fa[i]=i;
rank[i]=1;
}
}
合并(按秩合并)
void merge(int i,int j)
{
int x = find(i),y = find(j);
if(rank[x] <= rank[y]) fa[x] = y;
else fa[y] = x;
if(rank[x] == rank[y] && x!=y) rank[y] ++;//深度相同,新的根节点需要+1
}
并查集的题目练习
亲戚问题
题目背景
若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。
题目描述
规定:x和y是亲戚,y和z是亲戚,那么x和z也是亲戚。如果x,y是亲戚,那么x的亲戚都是y的亲戚,y的亲戚也都是x的亲戚。
输入格式
第一行:三个整数n,m,p,(n<=5000,m<=5000,p<=5000),分别表示有n个人,m个亲戚关系,询问p对亲戚关系。
以下m行:每行两个数Mi,Mj,1<=Mi,Mj<=N,表示Mi和Mj具有亲戚关系。
接下来p行:每行两个数Pi,Pj,询问Pi和Pj是否具有亲戚关系。
输出格式
P行,每行一个’Yes’或’No’。表示第i个询问的答案为“具有”或“不具有”亲戚关系。
代码:
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cmath>
using namespace std;
const int N=1000;
int fa[N],r[N];
//初始化
void init(int n)
{
for(int i=1;i<=n;i++)
{
fa[i]=i;
r[i]=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)
{
int x = find(i),y = find(j);
if(r[x] <= r[y]) fa[x] = y;
else fa[y] = x;
if(r[x]==r[y] && x!=y) r[y]++;
}
int main()
{
int n,m,p;
cin>>n>>m>>p;
init(n);//初始化
int i,j;
while(m--)
{
cin>>i>>j;
merge(i,j);//合并亲戚关系
}
int pi,pj;
while(p--)
{
cin>>pi>>pj;
if(find(pi) == find(pj))
{
cout<<"YES"<<endl;
}
else cout<<"NO"<<endl;
}
return 0;
}