来源:亲戚 - 洛谷
目录
题目背景
若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。
题目描述
规定:xx 和 yy 是亲戚,yy 和 zz 是亲戚,那么 xx 和 zz 也是亲戚。如果 xx,yy 是亲戚,那么 xx 的亲戚都是 yy 的亲戚,yy 的亲戚也都是 xx 的亲戚。
输入格式
第一行:三个整数 n,m,pn,m,p,(n,m,p≤5000n,m,p≤5000),分别表示有 nn 个人,mm 个亲戚关系,询问 pp 对亲戚关系。
以下 mm 行:每行两个数 MiMi,MjMj,1≤Mi, Mj≤n1≤Mi, Mj≤n,表示 MiMi 和 MjMj 具有亲戚关系。
接下来 pp 行:每行两个数 Pi,PjPi,Pj,询问 PiPi 和 PjPj 是否具有亲戚关系。
输出格式
pp 行,每行一个 Yes
或 No
。表示第 ii 个询问的答案为“具有”或“不具有”亲戚关系。
输入输出样例
输入#1
6 5 3 1 2 1 5 3 4 5 2 1 3 1 4 2 3 5 6
输出 #1
Yes Yes No
分析
一道经典的并查集裸题,先来回忆一下什么是并查集。
所谓并查集,就是对一个集合进行合并、查找。并查集是一种树型集合,用来处理不相交的集合的合并以及数据的查找的问题。
集合的合并也就是树的合并。
如果有n个数据,m对关系,每一对关系中有两个数据,表示这两个数据属于一个集合,根据已知关系构造并查集。
比如,现在有6个数据:1 2 3 4 5 6
3对关系:4 3 3 2 2 1
所以有3个集合。
画个图来直观的感受一下:
因为2,3是一个集合,所以1,2,3,4都是一个集合。
(就好比朋友的朋友也是朋友)
每次询问:给出的两个数据是否在同一个集合中?
比如输入4和1,输出:是;输入2和6,输出:否。
以上就是并查集的裸题。
下面来详细解释一下合并和查询。
合并:把数据所在的集合都合并在一起。通过根结点来合并,也就是让其中一个根结点去做另一个根结点的父亲结点。
比如现在有两棵树,一棵树的根结点是A,另一棵树的根结点是B:
现在进行合并操作,让A做B的根结点,就可以将这两棵树合并:
相信读者也会想到,合并之前要先找到每棵树的根结点。
下面介绍三种方法来找根结点:
首先我们创建一个父亲数组,用来存对应结点的父亲结点数据
int father[1024]; // 双亲数组:father[i] = x,i的父亲结点是x
初始化并查集时,我们先让所有结点的父亲结点赋值为本身(一开始所有的结点都独立地是一棵树),设根结点的父亲结点是其本身,因此判断一个结点是否是根结点的依据:x == father[x];
1、非递归版本
因为根结点的父亲结点是其本身,因此我们只要遍历要查找的结点以及其所有的祖先结点,判断它们是否是根结点。
// 查找x的根结点,并返回x的根结点(非递归版本)
int findRoot(int x){
while(x != father[x]){ // 根结点的父亲结点是其本身
x = father[x];
}
return x;
}
2、递归版本
如果当前结点不是根结点,那么就看其父亲结点是否是根结点,一直找到根结点为止,递归出口即为x != father[x];(感兴趣的读者可以自行画一下递归调用图感受一下递归过程)
// 查找x的根结点,并返回x的根结点(递归版本)
int findRoot(int x){
if(x != father[x]){
return findRoot(father[x]); // x != father[x],说明x不是根结点,那么就看其父亲结点是否是根结点
}
else{ // x == father[x]
return x;
}
}
3、路径压缩
路径压缩是并查集的一种优化方式, 是在第二种方法上进行改进。每次递归调用后,我们将当前结点的父亲结点赋值为递归调用的结果,这样,递归结束后,要查找的结点和它到根结点这条路径上的所有祖先结点都成为了根结点的孩子(这些结点的高度都变为了2)。
比如对C进行查找根结点的操作:
查找结束后,树变成了这样:
伪代码:
// 递归查找的另一个版本
// 执行以下代码后,根结点成为了要查找的结点到根结点这条路径上(查找路径)所有结点的父亲结点
// 路径压缩
int findRoot(int x){
if( x != father[x]){
return father[x] = findRoot(father[x]);
// 也可写成 father[x] = findRoot(father[x]);
// return father[x];
}
else{
return x;
}
}
查找:判断两个数据是否在同一集合中,就是去看两个数据所在的结点在不在同一棵树中,即向上找根结点,如果根结点相同,就在同一棵树中,也就是在同一个集合中。
// 查找
cout << "请输入要查询的次数:" << endl;
cin >> q;
cout << "----------------------------------------------------" << endl;
for(int i = 1; i <= q; i++){
cout << "第" << i << "次查询:" << endl;
// 判断两个数据是否在同一个集合中:看这两个数据是否在同一棵树中,也就是分别查找他们的根结点并判断,如果根结点一样就在同一棵树中
cout << "请分别输入要查询的两个数据:" << endl;
cin >> x >> y;
rx = findRoot(x); // rx是x所在树的根结点
ry = findRoot(y); // ry是y所在树的根结点
if(rx == ry){
cout << x << "和" << y << "在同一个集合中" << endl;
}
else{
cout << x << "和" << y << "不在同一个集合中" << endl;
}
cout << "----------------------------------------------------" << endl;
}
再回到本题,两个是亲戚的人属于一个集合,集合的合并就是树的合并,判断两个人是不是亲戚,就是判断他们是不是属于一个集合,也就是看这两个结点有没有相同的根结点。
代码
#include <iostream>
using namespace std;
int father[10005];
int findRoot(int x);
int main(){
int n, m, p; // n个人,m个亲戚关系,询问p对
cin >> n >> m >> p;
for(int i = 1; i <= n; i++){
father[i] = i;
}
int x, y, rx, ry;
for(int i = 1; i <= m; i++){
cin >> x >> y;
rx = findRoot(x);
ry = findRoot(y);
father[rx] = ry;
}
for(int i = 1; i <= p; i++){
cin >> x >> y;
rx = findRoot(x);
ry = findRoot(y);
if(rx == ry){
cout << "Yes" << endl;
}
else{
cout << "No" << endl;
}
}
system("pause");
return 0;
}
int findRoot(int x){
if(x != father[x]){
return father[x] = findRoot(father[x]);
}
else{
return x;
}
}