算法讲解057 必备 并查集-下

来源:算法讲解057【必备】并查集-下 · 2026-10-22
依据:视频字幕 2000 字

一句话概括

并查集(DSU)用“集合标签/集合属性”把同行同列、同属性、同连通块的元素合并,答案往往是 N - 集合个数 或“每个集合维护一个聚合量”。

它解决什么问题

基础知识(前置概念,用大白话解释)

原理(为什么这么做是对的,尽量给直觉)

以“移除最多同行或同列石头”为例:

合并方式:不需要两两枚举石头。对每一行维护“这一行第一次出现的石头编号”,对每一列同理;新石头到来时,如果该行/列已有石头,就合并。这样每个石头最多触发两次合并。

步骤(编号,可照着写)

  1. 读入 n 个石头,每个石头有 (x, y)。
  2. 初始化并查集,fa[i] = i,sz[i] = 1,集合数 cnt = n。
  3. 准备两个映射:rowFirst:行号 -> 该行第一个石头编号;colFirst:列号 -> 该列第一个石头编号。
  4. 对每个石头 i:

- 若 rowFirst 中已有行号 x,设对方为 j,union(i, j);否则 rowFirst[x] = i。

- 若 colFirst 中已有列号 y,设对方为 j,union(i, j);否则 colFirst[y] = i。

  1. 每次成功合并(两个不同集合)时 cnt--。
  2. 答案:n - cnt(最多删除数),或 cnt(最少剩余数)。

样例(一个具体小例子,带每步变化)

石头:(0,0), (0,1), (1,1), (2,2),n=4。

| 步骤 | 石头 | 行映射 | 列映射 | 合并 | cnt |

|---|---|---|---|---|---|

| 初始 | - | 空 | 空 | - | 4 |

| 1 | (0,0) | row[0]=0 | col[0]=0 | 无 | 4 |

| 2 | (0,1) | row[0]已有0 | col[1]=1 | union(1,0) | 3 |

| 3 | (1,1) | row[1]=2 | col[1]已有1 | union(2,1) | 2 |

| 4 | (2,2) | row[2]=3 | col[2]=3 | 无 | 2 |

最终 cnt=2,最多删除 4-2=2。两个连通块:{0,1,2} 和 {3},各剩 1 个。

代码(Python 一份:好读、用于理解;C++ 一份:竞赛风,能直接抄进比赛,带注释,不用玄学宏)

Python(好读版)

class DSU:
    def __init__(self, n):
        self.fa = list(range(n))
        self.sz = [1] * n
        self.cnt = n  # 集合个数

    def find(self, x):
        while self.fa[x] != x:
            self.fa[x] = self.fa[self.fa[x]]
            x = self.fa[x]
        return x

    def union(self, a, b):
        ra, rb = self.find(a), self.find(b)
        if ra == rb:
            return False
        if self.sz[ra] < self.sz[rb]:
            ra, rb = rb, ra
        self.fa[rb] = ra
        self.sz[ra] += self.sz[rb]
        self.cnt -= 1
        return True


def remove_stones(stones):
    n = len(stones)
    dsu = DSU(n)
    row_first = {}
    col_first = {}
    for i, (x, y) in enumerate(stones):
        if x in row_first:
            dsu.union(i, row_first[x])
        else:
            row_first[x] = i
        if y in col_first:
            dsu.union(i, col_first[y])
        else:
            col_first[y] = i
    return n - dsu.cnt

C++(竞赛风,可直接抄)

#include <bits/stdc++.h>
using namespace std;

struct DSU {
    vector<int> fa, sz;
    int cnt; // 当前集合个数
    DSU(int n = 0) { init(n); }
    void init(int n) {
        fa.resize(n);
        sz.assign(n, 1);
        iota(fa.begin(), fa.end(), 0);
        cnt = n;
    }
    int find(int x) {
        while (fa[x] != x) {
            fa[x] = fa[fa[x]];
            x = fa[x];
        }
        return x;
    }
    bool unite(int a, int b) {
        int ra = find(a), rb = find(b);
        if (ra == rb) return false;
        if (sz[ra] < sz[rb]) swap(ra, rb);
        fa[rb] = ra;
        sz[ra] += sz[rb];
        --cnt;
        return true;
    }
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n;
    cin >> n;
    vector<int> x(n), y(n);
    for (int i = 0; i < n; ++i) cin >> x[i] >> y[i];

    DSU dsu(n);
    unordered_map<int, int> rowFirst, colFirst;
    rowFirst.reserve(n * 2);
    colFirst.reserve(n * 2);

    for (int i = 0; i < n; ++i) {
        auto itR = rowFirst.find(x[i]);
        if (itR != rowFirst.end()) dsu.unite(i, itR->second);
        else rowFirst[x[i]] = i;

        auto itC = colFirst.find(y[i]);
        if (itC != colFirst.end()) dsu.unite(i, itC->second);
        else colFirst[y[i]] = i;
    }

    cout << n - dsu.cnt << '\n';
    return 0;
}

复杂度(时间/空间,最好/平均/最坏,顺带说常数大不大)

竞赛里什么时候用(看到什么题面特征就该想到它)

竞赛模板(C++,可直接背下来的写法;变量名清楚)

struct DSU {
    vector<int> fa, sz;
    int cnt;
    DSU(int n = 0) { init(n); }
    void init(int n) {
        fa.resize(n);
        sz.assign(n, 1);
        iota(fa.begin(), fa.end(), 0);
        cnt = n;
    }
    int find(int x) {
        while (fa[x] != x) {
            fa[x] = fa[fa[x]];
            x = fa[x];
        }
        return x;
    }
    bool unite(int a, int b) {
        int ra = find(a), rb = find(b);
        if (ra == rb) return false;
        if (sz[ra] < sz[rb]) swap(ra, rb);
        fa[rb] = ra;
        sz[ra] += sz[rb];
        --cnt;
        return true;
    }
    bool same(int a, int b) { return find(a) == find(b); }
    int size(int x) { return sz[find(x)]; }
};

带集合标签的写法(例如集合内石头数、集合内最大行号):

struct DSU {
    vector<int> fa, sz;
    vector<int> maxRow; // 集合标签示例
    int cnt;
    void init(int n, const vector<int>& row) {
        fa.resize(n); sz.assign(n, 1); maxRow = row;
        iota(fa.begin(), fa.end(), 0);
        cnt = n;
    }
    int find(int x) {
        while (fa[x] != x) { fa[x] = fa[fa[x]]; x = fa[x]; }
        return x;
    }
    bool unite(int a, int b) {
        int ra = find(a), rb = find(b);
        if (ra == rb) return false;
        if (sz[ra] < sz[rb]) swap(ra, rb);
        fa[rb] = ra;
        sz[ra] += sz[rb];
        maxRow[ra] = max(maxRow[ra], maxRow[rb]); // 合并标签
        --cnt;
        return true;
    }
};

边界与易错点(WA 的常见原因,越具体越好)

同类题 / 变形(说清楚怎么改)

常见坑

拓展(变体、实际用途、面试常问)

自测 3 题(题目 + 折叠答案)

题 1:n=3 个石头 (0,0),(0,0),(1,1),最多删除几个?

答案

两个 (0,0) 同行同列合并,(1,1) 独立。集合数 2,答案 3-2=1。

</details>

题 2:n=5 个石头 (0,0),(0,1),(1,1),(2,2),(2,3),最多删除几个?

答案

(0,0)-(0,1)-(1,1) 一个集合;(2,2)-(2,3) 一个集合。集合数 2,答案 5-2=3。

</details>

题 3:如果题目要求输出“每个连通块内石头数量”,并查集要怎么改?

答案

合并时维护 sz[ra] += sz[rb];最后对每个 i 求 find(i),把 sz[find(i)] 累加或直接输出根节点的 sz。注意只输出根节点(find(i)==i)的 sz。

</details>