本文已参与「新人创作礼」活动, 一起开启掘金创作之路。
本来想写加权并查集,然后开头提了一嘴普通并查集,一不小心写太长了,于是单独成篇。
并查集是一种数据结构,主要功能是处理关系的传递性问题,下面我们简单进行说明。
问题定义
抽象的
如果集合 上的二元关系 具有自反性、对称性和传递性,则我们可以把 划分成若干个子集 ,使得 中每个元素存在且只存在于一个划分后的子集中,并满足以下条件:
- 若 ,则 。
- 若 ,则 。
可以用并查集实现以下三种种操作:
- 合并:如果我们新收到的信息表明 ,则对于给定的 ,合并集合 和 。
- 查询:于给定的 ,找到其所在集合。
- 判断:或者对于给定的 ,判断 的真值。
具体的
如洛谷上某个并查集模板题 P1551 亲戚 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)。
题目大意如下:
若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在我们规定 和 是亲戚, 和 是亲戚,那么 和 也是亲戚。
现在共有 个人,给出若干对 ,表示 和 具有亲戚关系。
对于若干组询问,求输入 两人是否具有亲戚关系。
思路
如果我们给每个输入的亲戚关系 ,都在 和 之间建一条边,那么容易发现每个连通块里所有的点两两间具有亲戚关系。但是如果我们按照输入的关系建出图来,对于每组查询都进行 DFS 来判断两个点是否在一个连通图内,时间复杂度难以接受。(本题中先给出了全部的合并操作再进行查询,可以离线所有查询建图分别遍历。但是在其他情况下我们往往合并和查询交题混杂进行,必须在线处理。)
我们发现查询无需确定的知道具体的关系,只需要知道两个节点是否在同一个集合里即可,我们可以在保证图的连通性不改变的情况下随意修改图的形状。
容易发现把每个连通子图抽象成一棵有根树是一个很好的选择,我们开一个数组 表示节点 的父节点是 ,一棵树的根节点作为整个集合的代表元素。在最初每个节点的父节点都指向其本身。则对于三种操作具体说明如下。
查询
对于给定的 ,找到其所在集合。
我们可以利用循环或递归。
只要 ,就对 进行查询操作,直到找到整个集合的根节点。以该根节点为根的节点均与 在同一集合。
判断
或者对于给定的 ,判断 的真值。
分别查询 和 所在集合的代表元素,记为 和 。
若 ,则 和 在同一个集合中,即 。
否则 。
合并
对于给定的 ,合并集合 和 。
分别查询 和 所在集合的代表元素,记为 和 。
令 。这样我们在未来找代表元素时,原本以 为根的树中的节点就会继续找到 ,从而达成合并集合的效果。
容易发现如果树高很高的情况下查询的运算次数依然很多,而我们可以在保证图的连通性不改变的情况下随意修改图的形状,所以我们在查询元素的代表元素时可以将路径上的节点的父节点直接指向根节点(路径压缩)。
代码
#include <iostream>
using namespace std;
int a[5001];
int n,m,p;
int find(int x)
{
if (a[x]==x) return x;
else return a[x]=find(a[x]);
}
int main()
{
int i,j,k;
cin>>n>>m>>p;
for (i=1;i<=n;++i) a[i]=i;
for (i=1;i<=m;++i)
{
int x,y;
cin>>x>>y;
if (find(x)!=find(y)) a[a[x]]=a[y];
}
for (i=1;i<=p;++i)
{
int x,y;
cin>>x>>y;
if (find(x)==find(y)) cout<<"Yes"<<endl;
else cout<<"No"<<endl;
}
return 0;
}