洛谷 - P1551 亲戚(并查集介绍)

题目

题目背景

若某个家族人员过于庞大,要判断两个是否是亲戚,确实还很不容易,现在给出某个亲戚关系图,求任意给出的两个人是否具有亲戚关系。

题目描述

规定: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;
}

  • 12
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值