【例4-7】亲戚(relation)

GitHub跳转原题关系图返回列表

把亲戚关系看成无向图的连通性,用带路径压缩与按大小合并的并查集合并全部关系,之后每次询问只比较两人的代表元。

OJ: roj

题目 ID: 1346

难度:普及-

标签:并查集连通性模板题python

创建: 2026-09-30 06:22

更新: 2026-10-13 22:33

题目描述

给出 NN 个人(编号 1∼N1 \sim N)与 MM 条已知亲戚关系(亲戚关系可以传递),再给 QQ 次询问,每次问两人是否是亲戚。 输入:第一行 N,MN, M;接下来 MM 行每行 ai,bia_i, b_i 表示 aia_i 和 bib_i 是亲戚;然后一行 QQ,接下来 QQ 行每行 ci,dic_i, d_i 表示一次询问,1≤N≤200001 \le N \le 20000,1≤M,Q≤1061 \le M, Q \le 10^6。输出:对每个询问输出一行,是亲戚输出 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;
}