把亲戚关系看成无向图的连通性,用带路径压缩与按大小合并的并查集合并全部关系,之后每次询问只比较两人的代表元。
OJ: roj
题目 ID: 1346
难度:普及-
标签:并查集连通性模板题python
创建: 2026-09-30 06:22
更新: 2026-10-13 22:33
前置题目
后置题目
luogu P1195口袋的天空B 的 Kruskal 直接复用 A 教的并查集模板:用 find 判断边两端是否同属一块,不同才合并计数连通块luogu P8686[蓝桥杯 2019 省 A] 修改数组B 把 A 教的「parent 数组 + find 沿父指针找根并路径压缩」原样用作后继并查集的求根(main.cpp 的 find_root 含 fa[x]=find_root(fa[x])),只把 A 的回答询问换成 fa[x]=find(x+1) 一路跳到下一个空位;A 只讲连通性模板,B 的难点是叠加在这层 find 骨架上的后继建模与取值域 V。luogu P2872[USACO07DEC] Building Roads SB 把 A 教的「parent 数组 + find 到根、根相同即同块、union 合并两块」原样用于 Kruskal:先 union 合并已有免费道路的端点,再对排好序的候选边用根是否相同判断能否连入并累加长度;A 只做连通块划分与询问比较,B 在此基础上叠加完全图取距离与边排序。luogu P3366【模板】最小生成树B 直接把 A 教的代表元判定(find 相同即同块)用作是否成环的判断,并把 A 用于建块的 union 换成按边权依次合并两端,A 是纯并查集模板,B 在其上叠加排序与选边流程,难度差 2 级
题目描述
给出 Yes,否则输出 No。
样例:输入 10 7 / 2 4 / 5 7 / 1 3 / 8 9 / 1 2 / 5 6 / 2 3 / 3 / 3 4 / 7 10 / 8 9(/ 表示换行),输出 Yes / No / Yes。
思路
「是亲戚」具有传递性,本题就是无向图连通性问题:把每条关系看成一条边,用按大小合并加路径压缩的并查集把全部关系合并完,之后每次询问只需比较两人的代表元是否相同。
参考代码
Python 版:
python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-30 06:22
# update_at: 2026-10-13 22:28
import sys
class DSU:
"""并查集 (Disjoint Set Union)"""
def __init__(self, n: int):
# 1-indexed, fa 初始指向自己,sz 初始为 1
self.fa = list(range(n + 1))
self.sz = [1] * (n + 1)
def find(self, x: int) -> int:
"""查找根节点,两趟路径压缩:先走到根,再把整条路径挂到根上"""
root = x
while self.fa[root] != root:
root = self.fa[root]
while self.fa[x] != root:
self.fa[x], x = root, self.fa[x]
return root
def same(self, x: int, y: int) -> bool:
"""判断 x 和 y 是否属于同一个集合"""
return self.find(x) == self.find(y)
def unite(self, x: int, y: int) -> bool:
"""按大小合并两个集合 (小树挂大树);已在同一集合返回 False"""
fx, fy = self.find(x), self.find(y)
if fx == fy:
return False
if self.sz[fx] < self.sz[fy]:
fx, fy = fy, fx
self.fa[fy] = fx
self.sz[fx] += self.sz[fy]
return True
def main() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data, None) # 首 token 是人数;空输入按原样直接返回
if n is None:
return
m = next(data) # 关系条数
dsu = DSU(n) # 初始各自成块
for _ in range(m):
a, b = next(data), next(data) # 一条关系连接 a、b
dsu.unite(a, b)
q = next(data) # 询问次数
out: list[str] = []
for _ in range(q): # 代表元相同即为亲戚
c, d = next(data), next(data)
out.append("Yes" if dsu.same(c, d) else "No")
sys.stdout.write("\n".join(out) + "\n")
if __name__ == "__main__":
main()C++ 版(同一算法):
cpp
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-05 11:15
* update_at: 2026-10-13 22:33
*/
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long ll;
// 并查集竞赛精简版(不用 struct):路径压缩 + 按大小合并
// 口诀:find 找根压到底,unite 小树挂大树
// 易错点:init 别只写 fa[i] = i,sz[i] 也要置 1;合并后 sz 只在新根上有效
// 和别的模板一起用时 maxn 只留一份
const int maxn = 20005;
int fa[maxn], sz[maxn]; // sz[x] 仅在根上有效
void init(int n) {
for (int i = 1; i <= n; i++) fa[i] = i, sz[i] = 1;
}
// 查 x 的根,顺手把整条链压到根上
int find(int x) {
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
bool same(int x, int y) { return find(x) == find(y); }
// 小树挂大树,返回是否真的发生了合并
bool unite(int x, int y) {
int fx = find(x), fy = find(y);
if (fx == fy) return false;
if (sz[fx] < sz[fy]) swap(fx, fy);
fa[fy] = fx;
sz[fx] += sz[fy];
return true;
}
int main() {
int n, m;
scanf("%d %d", &n, &m);
// 初始化:每个人自成一个集合
init(n);
// 读入 M 条亲戚关系,逐条合并
for (int i = 1; i <= m; i++) {
int a, b;
scanf("%d %d", &a, &b);
unite(a, b);
}
int q;
scanf("%d", &q);
// 每次询问只需比较两人的代表元
for (int i = 1; i <= q; i++) {
int c, d;
scanf("%d %d", &c, &d);
if (same(c, d))
printf("Yes\n");
else
printf("No\n");
}
return 0;
}