题目
题目背景
若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。
题目描述
规定:x 和 y 是亲戚,y 和 z 是亲戚,那么 x 和 z 也是亲戚。如果 x,y 是亲戚,那么 x 的亲戚都是 y 的亲戚,y 的亲戚也都是 x 的亲戚。
输入格式
第一行:三个整数 n,m,p(n,m,p≤5000),分别表示有 n 个人,m 个亲戚关系,询问 p 对亲戚关系。
以下 m 行:每行两个数 M_jMj,N1≤Mi, Mj≤N,表示 Mi 和 Mj 具有亲戚关系。
接下来 p 行:每行两个数 Pi,Pj,询问 Pi 和 Pj 是否具有亲戚关系。
输出格式
p 行,每行一个 Yes
或 No
。表示第 i 个询问的答案为“具有”或“不具有”亲戚关系。
输入输出样例
输入
6 5 3 1 2 1 5 3 4 5 2 1 3 1 4 2 3 5 6
输出
Yes Yes No
思路
这是一道纯纯的并查集样题。
简单的并查集
并查集是一种很简洁的数据结构,主要作用于元素分类的问题,它存放了一堆一堆的不相交的集合,并可对其进行创建、查询和合并三种操作。
创建:假如有n个元素,我们用一个数组a[n]来储存每一个元素的父节点(因为每个元素字可能有一个父节点),一开始我们要令它们的父节点为自己。
int a[MAXN];
void init(int n)
{
for (int i = 1; i <= n; ++i)
{
a[i] = i;
}
}
查询:查询两个元素是否为同一集合。
int find(int x)
{
if(a[x] == x)//如果待查询元素就是祖先元素
{
return x;
}
else//如果不是,则往上层寻找
{
return find(a[x]);
}
}
合并:将题目所描述的两个本不相关的元素合并成一个集合。
void merge(int x, int y)//这里是令x集合属于y集合
{
a[find(x)] = find(y);
}
并查集最核心的思想在于用集合中和祖先元素来代表整个集合。
有一个生动形象的比喻:整个并查集像一个江湖,分布着许多的武林高手
一开始谁都不认识谁,各自为营,所以对于每一个元素来说,每一个元素都是一个集合,也都代表着自己的集合。
后来1和2打起来了,1赢了,2就认1为老大,这个时候1和2就是一伙的了,并且1代表着这个集合。
接着3找到2,打一架,没打过,就甘拜下风,认2为老大,但2的老大是1,老大的老大当然还是老大,所以现在1,2,3都是一伙的了,并且还是1代表这个集合。
正当1,2,3,打得正嗨的时候,6早已经称霸一方了,这时候4,5,6,7为一个集合,6代表着这个集合。
这时候3想和4打架,但4表示,别和我打,要打和我老大6打,3也把自己的上头2叫出来,2把自己的上头1叫出来,这样就是1和6在打了,不妨令1赢了,那么6就要认1为老大,自然4,5,7也跟着认了。
经过整理后,这个并查集如下
显然,这是一个树状结构,要寻找代表这个集合的元素,只需一层层往上访问父节点,直到树的根节点即可,根节点的父节点也是它自己。
在本题中,只要是同一个并查集里的元素,那他们都应该互为亲戚。
路径压缩
在某些数据量较大的题目中,用简单的并查集效率是很低的,例如
这个时候合并2和3,2找到了1,就相当于1和3合并,于是
再来个4
以此类推,不难发现,这个链越长,从底部寻找根节点的路径就会越来越远
这时我们可以只需看根节点,用路径压缩的方法将每个元素到根节点的路径最短,如下
路径压缩
int find(int x)
{
if(x == a[x])
{
return x;
}
else
{
a[x] = find(a[x]);//父节点设为根节点
return a[x];//返回父节点
}
}
按秩合并
可能有人会认为,在路径压缩后,这个并查集是不是始终只有两层。其实并不是,由于路径压缩是在查询find函数里面进行的,所以只能压缩当前查询的路径,那还有更加效率的优化吗?
例如现在要把1和7合并,如果能选择,当然是让 1作为父节点了,因为集合1比集合7更深,因此合并的时候不会增加树的深度。
我们要用一个数组rk[n]来记录每个根节点对应在树中的深度,在初始化的时候每个根节点的深度自然是1,合并的时候可以比较两个根节点的rk值,把小的合并到大的。
但要注意的是,如果路径压缩和按秩合并一起使用,虽然时间复杂度会接近O(n),但是路径压缩的时候很有可能会破坏数组rk的准确性。
按秩合并
int a[MAXN], rk[MAXN]
//初始化时要把rk也初始化
void init(int n)
{
for (int i = 1; i <= n; ++i)
{
a[i] = i;
rk[i] = 1;
}
}
//按秩合并
void merge(int x, int y)
{
int rx = find(x), ry = find(y);//先找到待合并元素的根节点
if (rk[rx] <= rk[ry])//如果x所属的树的深度比y的小,那么x树向y树合并
{
a[rx] = ry;
}
else//反之
{
a[ry] = rx;
}
//如果两树深度相同且根节点不同,则新的根节点的深度+1,两树已在第一个if处合并
if (rk[rx] == rk[ry] && rx != ry)
{
++rk[ry];
}
}
为什么深度相同合并的时候深度加1
1和3要合并时,上述代码是将1树合并到3树中
很明显,合并之后的深度加1了,反之亦然。
本题代码
#include<iostream>
using namespace std;
int n, m, p, a[5005], rk[5005], m1, m2, p1, p2;
int find(int x)//查询根节点(这里没用路径压缩,因为会影响树的深度,上述有提到)
{
if (x == a[x])
{
return x;
}
else
{
return find(a[x]);
}
}
void merge(int x, int y)//按轶合并
{
int rx = find(x), ry = find(y);
if (rk[rx] <= rk[ry])
{
a[rx] = ry;
}
else if (rk[rx] > rk[ry])
{
a[ry] = rx;
}
if (rk[rx] == rk[ry] && rx != ry)
{
++rk[ry];
}
}
int main()
{
cin >> n >> m >> p;
for (int i = 1; i <= n; ++i)//初始化
{
a[i] = i;
rk[i] = 1;
}
for (int i = 1; i <= m; ++i)//输入一个关系就合并一次
{
cin >> m1 >> m2;
merge(m1, m2);
}
for (int i = 1; i <= p; ++i)
{
cin >> p1 >> p2;
if (find(p1) == find(p2))//如果两人属于同一个集合,则为亲戚
{
cout << "Yes" << endl;
}
else//反之
{
cout << "No" << endl;
}
}
return 0;
}