情景引入之踢馆风云
有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 1∼n,最开始每个数各自在一个集合中。
现在要进行 m m m 个操作,操作共有两种:
M a b
,将编号为 a a a 和 b b b 的两个数所在的集合合并,如果两个数已经在同一个集合中,则忽略这个操作;Q a b
,询问编号为 a a a 和 b b b 的两个数是否在同一个集合中;
输入格式
第一行输入整数 n n n 和 m m m。
接下来
m
m
m 行,每行包含一个操作指令,指令为 M a b
或 Q 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 1≤n,m≤105
输入样例:
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;
}