算法

并查集(DSU)

近 O(1) 合并与查找——动态连通性的首选数据结构

并查集概览

并查集(Disjoint Set Union,DSU)支持两种操作:Union(合并两个集合)和 Find(查询某元素属于哪个集合)。配合路径压缩与按秩合并,两种操作均摊近 O(1)(精确为反阿克曼函数 α(n))。

操作复杂度

操作均摊时间说明
find(x)O(α(n)) ≈ O(1)路径压缩后几乎常数
union(x, y)O(α(n)) ≈ O(1)按秩合并保持树高
空间O(n)parent[] 与 rank[] 数组

DSU vs BFS/DFS

场景推荐
离线批量边、Kruskal MSTDSU
在线动态连通性查询DSU
需要具体路径或层次BFS/DFS
有向图拓扑排序BFS/DFS

基础实现

两个优化缺一不可:

两大优化

  • 路径压缩:find() 时将沿途所有节点直接挂到根节点,使后续查询更快
  • 按秩合并:union() 时将较矮的树挂到较高的树根下,控制树高不超过 O(log n)
// DSU with path compression + union by rank
struct DSU {
    vector<int> parent, rank;
    int components;

    DSU(int n) : parent(n), rank(n, 0), components(n) {
        iota(parent.begin(), parent.end(), 0); // parent[i] = i
    }

    int find(int x) {
        if (parent[x] != x)
            parent[x] = find(parent[x]); // path compression
        return parent[x];
    }

    bool unite(int x, int y) {
        int rx = find(x), ry = find(y);
        if (rx == ry) return false; // already connected
        if (rank[rx] < rank[ry]) swap(rx, ry);
        parent[ry] = rx;             // attach shorter tree under taller
        if (rank[rx] == rank[ry]) rank[rx]++;
        components--;
        return true;
    }

    bool connected(int x, int y) { return find(x) == find(y); }
};

按大小 vs 按秩按秩(rank)合并与按大小(size)合并均可。按大小更直观(rank[] 换成 size[],初始值为 1),效果相同。

练习:并查集 — 连通分量计数代码练习
测试用例(3 个)
用例 1
输入:5 3 0 1 1 2 3 4
期望:2
用例 2
输入:4 0
期望:4
用例 3
输入:4 3 0 1 1 2 2 3
期望:1

加权并查集

加权 DSU 在 parent[] 之外维护 weight[] 表示节点到根的相对权重(如距离、比值)。find() 时同步压缩路径并累加权重,union() 时根据已知关系设置权重差。

典型题目

  • 399. 除法求值(边权为比值,加权 DSU 存对数或比值)
  • 1061. 字典序最小等价字符串(等价类合并)
  • 547. 省份数量(基础连通分量计数)

注意加权 DSU 的权重语义取决于问题定义。比值类问题常用乘法而非加法合并,需仔细推导路径压缩时的权重更新公式。

练习:并查集 — 冗余边检测代码练习
测试用例(3 个)
用例 1
输入:4 5 0 1 1 2 2 3 0 3 1 3
期望:4
用例 2
输入:3 3 0 1 1 2 0 2
期望:3
用例 3
输入:2 2 0 1 0 1
期望:2

常见应用模式

模式速查

模式说明典型题
连通分量数初始化 cnt=n,每次真正合并时 cnt--547 省份数量
冗余边检测依次加边,若两端已连通则为冗余边684 冗余连接
Kruskal MST按权升序加边,DSU 判断是否成环1584 最小代价连通所有点
字符串等价类字符映射到整数,按等价关系合并1061 字典序最小等价字符串
// Detect the first redundant edge in an undirected graph
int findRedundant(vector<pair<int,int>>& edges, int n) {
    DSU dsu(n + 1);
    for (auto& [u, v] : edges) {
        if (!dsu.unite(u, v)) return /* this edge is redundant */ u; // or track index
    }
    return -1;
}
// Kruskal's MST — sort edges by weight, add if not creating a cycle
int kruskal(int n, vector<tuple<int,int,int>>& edges) { // (weight, u, v)
    sort(edges.begin(), edges.end());
    DSU dsu(n);
    int totalCost = 0, edgesUsed = 0;
    for (auto& [w, u, v] : edges) {
        if (dsu.unite(u, v)) {
            totalCost += w;
            if (++edgesUsed == n - 1) break; // MST complete
        }
    }
    return totalCost;
}

典型题目

  • 547. 省份数量
  • 684. 冗余连接
  • 685. 冗余连接 II(有向图)
  • 721. 账户合并
  • 399. 除法求值(加权)
练习:并查集 — Kruskal 最小生成树代码练习
测试用例(3 个)
用例 1
输入:4 5 0 1 1 0 2 4 1 2 2 1 3 5 2 3 1
期望:4
用例 2
输入:3 3 0 1 10 1 2 5 0 2 3
期望:8
用例 3
输入:2 1 0 1 7
期望:7
练习:并查集 — 连通性查询代码练习
测试用例(3 个)
用例 1
输入:5 3 0 1 1 2 3 4 3 0 2 0 3 3 4
期望:1 0 1
用例 2
输入:3 1 0 1 2 0 1 1 2
期望:1 0
用例 3
输入:4 4 0 1 1 2 2 3 0 3 2 0 3 1 3
期望:1 1