并查集

并查集(Disjoint Set Union,简称 DSU 或 Union-Find)是一种非常精巧且高效的数据结构,主要用于处理不相交集合的合并查询问题。

它的核心功能可以用一句话概括:快速判断两个元素是否属于同一个集合,以及将两个不同的集合合并。

以下是 C++ 并查集的深度详解,包含原理、图解、代码模板及核心优化。


1. 核心概念与形象比喻

为了理解并查集,我们可以使用“帮派”“朋友圈”的比喻:

  • 元素(Element): 具体的某个人。
  • 集合(Set): 一个帮派。
  • 代表元(Representative/Root): 帮派的“老大”。

并查集维护了一个森林(若干棵树),每一棵树代表一个集合。

  • 树根(Root): 就是这个集合的“老大”。
  • 父节点指针: 每个节点都只知道自己的上级(父节点)是谁。

三大操作

  1. 初始化 (Init): 开始时,每个人都是独立的,自己是自己的老大。
  2. 查询 (Find): 查找某个人的“最终大BOSS”是谁。如果两个人的大BOSS相同,说明他们在同一个帮派。
  3. 合并 (Union): 两个帮派合并。通常是让其中一个帮派的老大,成为另一个帮派老大的下属。

2. 核心操作与代码实现

2.1 初始化 (Initialization)

我们通常使用一个数组 parent (或 fa),其中 parent[i] 表示元素 $i$ 的父节点。

初始时,parent[i] = i,表示自己是自己的根节点。

#include <vector>
#include <numeric> // for std::iota

class DSU {
private:
    std::vector<int> parent;
public:
    DSU(int n) {
        parent.resize(n + 1); // 假设节点从 0 到 n 或 1 到 n
        // 初始化:每个人的父节点是自己
        // std::iota 填充 0, 1, 2... 也可以用循环
        for (int i = 0; i <= n; ++i) {
            parent[i] = i;
        }
    }
};

2.2 查询 (Find) - 核心优化:路径压缩

这是并查集最关键的一步。

  • 普通查找: 沿着父节点一路向上找,直到找到根节点(parent[x] == x)。
  • 路径压缩 (Path Compression): 在查找的过程中,直接把沿途经过的所有节点的父节点,全部改为指向根节点。

为什么需要路径压缩?

如果不压缩,树可能会退化成一条长链,查找效率变成 $O(n)$。压缩后,树的高度几乎是常数,下次再查该路径上的节点时,一步就能找到根。

    int find(int x) {
        if (parent[x] == x) {
            return x; // 找到根节点
        }
        // 递归查找,并将结果赋值给 parent[x] (路径压缩)
        return parent[x] = find(parent[x]); 
    }

2.3 合并 (Union)

将元素 $x$ 和元素 $y$ 所在的集合合并。

  1. 找到 $x$ 的根 $rootX$。
  2. 找到 $y$ 的根 $rootY$。
  3. 如果 $rootX \neq rootY$,则令 parent[rootX] = rootY(即让 $X$ 的帮派归顺 $Y$)。
    void unite(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);
        
        if (rootX != rootY) {
            parent[rootX] = rootY; // 合并
        }
    }
    
    // 辅助功能:判断是否相连
    bool isConnected(int x, int y) {
        return find(x) == find(y);
    }

3. 进阶优化:按秩合并 (Union by Rank/Size)

虽然路径压缩已经足够高效(能满足绝大多数算法竞赛和工程需求),但为了理论上的极致,我们可以结合按秩合并

  • 原理: 合并时,总是将“树高较小”或“节点数较少”的树,挂在“树高较大”或“节点数较多”的树下。
  • 目的: 防止树的高度增加过快。

加入按秩合并后的完整 C++ 模板:

#include <vector>

class DSU {
private:
    std::vector<int> parent;
    std::vector<int> rank; // 用于记录树的高度(大致高度)

public:
    DSU(int n) {
        parent.resize(n + 1);
        rank.resize(n + 1, 1); // 初始高度都为 1
        for (int i = 0; i <= n; ++i) {
            parent[i] = i;
        }
    }

    int find(int x) {
        if (parent[x] != x) {
            parent[x] = find(parent[x]); // 路径压缩
        }
        return parent[x];
    }

    void unite(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);

        if (rootX != rootY) {
            // 按秩合并:小树挂在大树下
            if (rank[rootX] < rank[rootY]) {
                parent[rootX] = rootY;
            } else if (rank[rootX] > rank[rootY]) {
                parent[rootY] = rootX;
            } else {
                parent[rootX] = rootY;
                rank[rootY] += 1; // 只有两棵树高度相等时,合并后高度才加 1
            }
        }
    }
    
    bool isConnected(int x, int y) {
        return find(x) == find(y);
    }
};

4. 时间复杂度分析

并查集的时间复杂度非常惊人。

如果同时使用路径压缩和按秩合并,单次操作的平均时间复杂度为:$$O(\alpha(n))$$

其中 $\alpha$ 是阿克曼函数 (Ackermann Function) 的反函数。

  • 它的增长极其缓慢。
  • 对于宇宙中所有实际可见的 $n$(例如 $10^{100}$),$\alpha(n) < 4$。
  • 因此,我们可以认为并查集的操作时间复杂度是近乎常数的。

5. 实战应用场景

并查集主要解决两类问题:

  1. 图的连通性问题: 比如“判断两点之间是否存在路径”、“计算岛屿数量”、“朋友圈个数”。
  2. 图的最小生成树 (MST): Kruskal 算法的核心就是并查集。

示例:计算连通分量个数 (省份数量)

假设有 n 个城市,isConnected[i][j] = 1 表示直接相连。求有多少个独立的省份。

int findCircleNum(std::vector<std::vector<int>>& isConnected) {
    int n = isConnected.size();
    DSU dsu(n);
    int count = n; // 初始假设有 n 个独立省份

    for (int i = 0; i < n; ++i) {
        for (int j = i + 1; j < n; ++j) {
            if (isConnected[i][j] == 1) {
                // 如果两城相连,且原本不在一个集合,则合并,总数减一
                if (dsu.find(i) != dsu.find(j)) {
                    dsu.unite(i, j);
                    count--;
                }
            }
        }
    }
    return count;
}

6. 总结

特性 说明
主要功能 集合合并、查询元素所属集合
核心技巧 路径压缩 (必须掌握)、按秩合并 (选学)
空间复杂度 $O(n)$
时间复杂度 $O(\alpha(n))$ (近乎常数)
典型应用 最小生成树(Kruskal)、连通性检测、最近公共祖先(Tarjan)
posted @ 2025-12-19 10:06  belief73  阅读(2)  评论(0)    收藏  举报