【数据结构】并查集

情景引入之踢馆风云

有10位武林高手,姑且称之为[1]号到[10]号高手。
他们分别创建了各自的武馆,各自悬挂1号到10号牌,我们就分别称之为1号到10号武馆。
请添加图片描述
第1天,1号高手跑去2号武馆踢馆,挑战方胜利,踢馆成功以后,2号高手的2号武馆就变成了1号高手的1号武馆的分馆,所以需要摘掉2号牌,改为悬挂1号高手的1字牌。请添加图片描述
第2天2号高手跑去踢3号的武馆,他担心自己功夫不够强,便请1号出手。挑战方胜利,踢馆成功之后,所以3号馆摘牌以后,也换成了1号招牌,也变成了1号的分馆。
请添加图片描述
第3天,3号去踢了4号的武馆,他也担心自己功夫不够强,便请1号出手。结果挑战方胜利,于是4号馆也变成了1号的分馆,4号馆摘牌,也换成了1号招牌。
请添加图片描述
第4天,5号看别人踢的挺热闹,自己也想去踢馆,于是就跑去踢3号馆。3号说就算你赢了我,想换成你的5号牌,怕是我的老大不答应吧,5号问你的老大是谁呀,3号一指自己所挂的1号牌。骄傲的说,我的老大当然是1号高手了。你等着,我这就把老大请过来教训你。结果呢,虽然3号请来1号出手,但挑战方胜利的规律是不变的,5号把1号打败了,于是1号馆无奈换成了5号牌。
请添加图片描述
第5天,2号去踢5号馆。5号说,你太嫩啦,还是把你老大找来吧。2号去请1号出手,到1号馆才发现1号馆已经改挂5号牌啦,于是赶紧把自己的牌子也换成5号牌,还得赶紧向5号赔礼道歉:“大水冲了龙王庙啊,我是有眼不识泰山,无意中冒犯了总馆主,请您恕罪啊!”5号笑笑:“算啦,回去吧。”这馆就没踢成。
请添加图片描述
第6天,10号踢了9号的馆,挑战方胜利,9号馆换10号牌变成了10号的分馆。
请添加图片描述
第7天,9号再去踢8号馆,他也担心自己功夫不够强,便请10号出手。挑战方胜利,于是8号馆也换成10号牌变成了10号的分馆。
请添加图片描述
第8天,7号跑去踢10号馆,挑战方胜利,结果就是10号馆换7号牌变成7号的分馆,由于丢人,10号不好意思告诉8号和9号跟着一起换7号牌,所以8号和9号现在挂的还是10号牌。
请添加图片描述
第9天,4号去踢9号馆,双方都请出各自的最强高手出手。4号现在挂着1号牌,去请1号时发现1号已经挂5号牌啦,于是换5号牌,改请5号出手挑战。9号现在挂着10号牌,去请10号时发现10号已经挂7号牌啦,于是换7号牌,改请7号出手迎战。挑战方胜利,7号馆换5号牌。
请添加图片描述
经过一番踢馆风云换牌吞并,武林格局已发生了重大变化。究竟还剩下多少独立门派呢?
只需要数数有几个总馆主就知道啦,因为所有挂别人牌子改换门庭的馆主都是被吞并的。只有总馆主才会保留自己的牌子。
请添加图片描述

例题

一共有 n n n 个数,编号是 1 ∼ n 1 \sim n 1n,最开始每个数各自在一个集合中。

现在要进行 m m m 个操作,操作共有两种:

  1. M a b,将编号为 a a a b b b 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;
  2. Q a b,询问编号为 a a a b b b 的两个数是否在同一个集合中;
输入格式

第一行输入整数 n n n m m m

接下来 m m m 行,每行包含一个操作指令,指令为 M a bQ a b 中的一种。

输出格式

对于每个询问指令 Q a b,都要输出一个结果,如果 a a a b b b 在同一集合内,则输出 Yes,否则输出 No

每个结果占一行。

数据范围

1 ≤ n , m ≤ 1 0 5 1 \le n,m \le 10^5 1n,m105

输入样例:
4 5
M 1 2
M 3 4
Q 1 2
Q 1 3
Q 3 4
输出样例:
No
Yes

什么是并查集

并查集常用于快速计算森林中有多少棵树。
并指合并操作,查指递归追查祖先,集指处理对象是集合。

如何实现并查集

初始化并查集

用一个一维数组 p p p 模拟并查集。开始时每个集合都是以自己为祖先(也就是最开始,每个武馆的总馆主都是自己)。
即:

for (int i = 1; i <= n; ++ i ) p[i] = i;	//	n为结点的个数(即数的个数)

合并操作

我们需要先实现一个函数:查找当前节点的祖先节点(也就是武馆的总馆主),在寻找祖先节点的过程中,使用类似记忆化搜索的方法同步更新父节点,这就是一种叫做路径压缩优化的技巧,可以有效减少追查深度。
代码:

int find(int x) {	//	查找x的祖先节点(路径压缩)
    if (p[x] != x) p[x] = find(p[x]);	//	祖先节点的父节点不是自己时更新父节点
    return p[x];
}

合并时,将两个集合的祖宗节点换掉就可以了

p[find(x)] = find(y);

查询操作

这个直接判断两个节点的祖宗节点是不是同一个节点就可以了,代码:

if (find(x) == find(y)) puts("Yes");
else puts("No");

例题实现代码

#include <iostream>
using namespace std;
const int N = 100010;
int n, m;
int p[N];
int find(int x) {
    if (p[x] != x) p[x] = find(p[x]);
    return p[x];
}
int main() {
    cin >> n >> m;
    for (int i = 1; i <= n; ++ i ) p[i] = i;
    while ( m -- ) {
        string op;
        int x, y;
        cin >> op >> x >> y;
        x = find(x), y = find(y);
        if (op == "M")  p[x] = y;
        else {
            if (x == y) puts("Yes");
            else puts("No");
        }
    }
    return 0;
}
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值