Skip to content

并查集(Union-Find)

为什么需要并查集

给你一堆元素,不断地做两件事:把两个集合合并、查某两个元素是不是同一个集合里的。听起来简单,但如果每次都遍历整个集合去找,效率极其拉胯。并查集就是专门解决这类"动态连通性"问题的数据结构——Kruskal 算法判断加边会不会成环,靠的就是它。

408 考研中并查集是树与森林章节的延伸考点,常与 Kruskal 算法结合出题,选择题考操作过程和复杂度,应用题考手算森林状态。

核心思想

并查集用森林表示若干不相交集合,每棵树代表一个集合,树的根节点就是这个集合的代表元素

  • 双亲表示法存储:用一个 parent[] 数组,parent[i] 记录元素 i 的父节点;根节点的 parent 值为 -1(或指向自身)
  • Find 操作:从给定元素沿 parent 链一路向上,直到找到根节点,返回根节点编号——两个元素的根相同,说明在同一集合
  • Union 操作:分别找到两个元素所在集合的根,然后让其中一个根指向另一个根,两棵树合并为一棵
  • 优化手段:路径压缩(Find 时将路径上的节点直接挂到根)和按秩合并(Union 时矮树挂到高树下),可以把操作复杂度压到接近 O(1)

操作详解

存储结构——双亲表示法

c
#define MAX 100
int parent[MAX];  // parent[i] 存储元素 i 的父节点,根节点为 -1

// 初始化:每个元素独立成集合
void Init(int n) {
    for (int i = 0; i < n; i++)
        parent[i] = -1;  // -1 表示根节点
}

初始状态下,每个元素的 parent 都是 -1,意味着每个元素自己就是一棵树的根,整个森林有 n 棵单节点的树。

Find 操作

朴素版:从元素 x 出发,沿 parent 链一直向上走,直到遇到 parent[x] == -1 的根节点。

c
// 朴素 Find:返回 x 所在集合的根
int Find(int x) {
    while (parent[x] != -1)
        x = parent[x];
    return x;
}

最坏情况下树退化为链表,每次 Find 的时间复杂度为 O(n)

路径压缩版:在 Find 的过程中,把路径上经过的所有节点直接挂到根节点下,下次再查这些节点就是 O(1)。

c
// 路径压缩 Find(递归版)
int Find(int x) {
    if (parent[x] == -1)
        return x;
    parent[x] = Find(parent[x]);  // 递归找根,顺便把 x 直接挂到根
    return parent[x];
}

// 路径压缩 Find(迭代版)
int Find(int x) {
    int root = x;
    while (parent[root] != -1)  // 先找到根
        root = parent[root];
    while (x != root) {          // 再把路径上的节点全部直接挂到根
        int next = parent[x];
        parent[x] = root;
        x = next;
    }
    return root;
}

⚠️ 易错:路径压缩只改变 Find 路径上节点的 parent 指向,让它们直接指向根。集合的逻辑划分没有任何变化——同一集合里的元素还是那些元素,只是树的形态变扁了。

Union 操作

朴素版:找到两个元素的根,直接让一个根指向另一个根。

c
// 朴素 Union:合并 x 和 y 所在的集合
void Union(int x, int y) {
    int rx = Find(x);
    int ry = Find(y);
    if (rx != ry)
        parent[rx] = ry;  // 让 rx 的根指向 ry
}

按秩合并版:维护一个 rank[] 数组记录每棵树的高度上界,合并时让矮树挂到高树下面,避免树越长越高。

c
int rank[MAX];  // rank[i] 表示以 i 为根的树的高度上界

void Init(int n) {
    for (int i = 0; i < n; i++) {
        parent[i] = -1;
        rank[i] = 0;  // 初始每棵树高度为 0
    }
}

// 按秩合并
void Union(int x, int y) {
    int rx = Find(x);
    int ry = Find(y);
    if (rx == ry) return;          // 已在同一集合
    if (rank[rx] < rank[ry]) {
        parent[rx] = ry;           // 矮树挂到高树下
    } else if (rank[rx] > rank[ry]) {
        parent[ry] = rx;
    } else {
        parent[ry] = rx;           // 等高时任选一个作根
        rank[rx]++;                // 新树高度 +1
    }
}

⚠️ 易错:按秩合并的 rank 在路径压缩后不再精确表示树高,只是一个上界。路径压缩会降低实际树高,但不会去更新 rank 值——因为精确维护 rank 代价太大,而且上界已经足够保证复杂度。考研选择题偶尔会考这个细节。

完整手算示例

初始集合 {0, 1, 2, 3, 4},执行操作序列:Union(0,1), Union(2,3), Union(1,3), Union(0,4)。使用朴素 Union(让前者的根指向后者的根)。

初始状态: 每个元素独立(parent 均为 -1)
  0    1    2    3    4

第1步: Union(0, 1) — Find(0)=0, Find(1)=1, parent[0]=1
  1    2    3    4
  |
  0

第2步: Union(2, 3) — Find(2)=2, Find(3)=3, parent[2]=3
  1    3    4
  |    |
  0    2

第3步: Union(1, 3) — Find(1)=1, Find(3)=3, parent[1]=3
       3    4
      / \
     1   2
     |
     0

第4步: Union(0, 4) — Find(0): 0→1→3, 根为3; Find(4)=4, parent[3]=4
         4
         |
         3
        / \
       1   2
       |
       0

parent 数组变化过程

操作parent[0]parent[1]parent[2]parent[3]parent[4]
初始-1-1-1-1-1
Union(0,1)1-1-1-1-1
Union(2,3)1-13-1-1
Union(1,3)133-1-1
Union(0,4)1334-1

应用:Kruskal 算法

Kruskal 算法在构建最小生成树时,需要判断一条边的两个端点是否已在同一连通分量中(加入则会成环)。并查集完美适配这个需求:

  1. 初始化并查集,每个顶点独立成集合
  2. 将所有边按权值排序
  3. 依次取权值最小的边 (u, v),执行 Find(u)Find(v)
  4. 若根不同,说明 u、v 不连通,选中该边并 Union(u, v)
  5. 若根相同,跳过该边(加入会形成回路)

等价类划分

并查集也可用于等价类划分问题:给出若干"等价关系"对 (a, b),将所有等价的元素归入同一类。每读入一对 (a, b),执行 Union(a, b) 即可,最终 Find 值相同的元素属于同一等价类。

复杂度分析

实现方式FindUnion说明
朴素实现O(n)O(n)Union 包含两次 Find,树可能退化为链
路径压缩O(log n)O(log n)Find 时压缩路径,均摊复杂度
按秩合并O(log n)O(log n)限制树高,最坏 O(log n)
路径压缩 + 按秩合并O(α(n))O(α(n))α 为反阿克曼函数,实际可视为 O(1)

空间复杂度:O(n),需要 parent[] 数组(按秩合并额外需要 rank[] 数组)。

α(n) 是反阿克曼函数,增长极其缓慢。对于任何实际可能出现的 n 值(哪怕是宇宙中的原子数),α(n) 都不超过 5。408 考试中一般只要求知道"接近常数"即可。

考研高频考点

  • ⭐ 给定操作序列,画出并查集的森林状态(应用题高频,手算 parent 数组变化)
  • ⭐ 路径压缩的过程与效果(选择题考"压缩后的树形态")
  • ⭐ 按秩合并的规则(矮树挂高树,等高则任选并 rank+1)
  • ⭐ Find 和 Union 操作的时间复杂度(朴素 vs 优化后的对比)
  • ⭐ 并查集在 Kruskal 算法中的应用(判断是否成环)
  • ⭐ 并查集的存储结构——双亲表示法(parent 数组的含义与初始化)
  • 等价类划分问题的求解(Union-Find 的直接应用场景)
  • 路径压缩与按秩合并同时使用时的复杂度为 O(α(n))(选择题常考的结论)

相关知识

  • 哈夫曼树:同样是自底向上合并的树型结构,贪心策略有异曲同工之处
  • 树与森林的转换:并查集本质上是用森林表示集合,涉及树的双亲表示法
  • Kruskal 算法:Kruskal 算法的核心依赖并查集判断连通性

真题练习