这一篇在干嘛?

动态等价问题:一堆元素,反复问”a 和 b 在同一个集合里吗?“以及”把 a、b 所在的集合合并”。并查集(Union-Find/不相交集森林)用少到离谱的代码把两个操作都做到近乎 (摊还 是反阿克曼函数,实战中 ≤ 4)。它是 Kruskal 最小生成树、迷宫生成、连通分量判定的核心零件。

8.1 等价关系

集合 上的关系 等价关系,当且仅当满足三条:

  • 自反
  • 对称
  • 传递

是自反+传递但不对称;相等 = 是完美等价关系。等价关系把集合切成互不相交的等价类——动态等价问题的目标就是在线维护这些类。

8.2 两个操作

  • find(x):返回 x 所在等价类的代表(类内所有元素返回同一个名字,类间不同)。
  • union(x, y):把 x 和 y 所在的两个类合并成一个。

注意算法的”感受不到名字”特性:find(a) == find(b) 判等价,但具体代表是谁无所谓——只要同一类内一致。

8.3 基础结构:森林 + 父指针数组

每个集合是一棵树;s[i] 存节点 i 的父亲下标,根的父亲存自己(或特殊标记):

// find:一路向上找根
int find(int x) {
    while (s[x] >= 0)     // 根的值是负的(可兼存集合大小)
        x = s[x];
    return x;
}
 
// 朴素 union:把一棵树挂到另一棵下面
void unionSets(int root1, int root2) {
    s[root2] = root1;
}

图:8 个元素初始各自成集(父亲都是 −1)

图:union(4,5) 后——4 当根,5 挂上来

图:union(6,7)、union(4,6) 连续合并——树在长高

朴素 union 的隐患:乱挂会长出一条链,find 退化 ——最坏树就是逐个挂形成的链(N=16 时深度可达 15)。

图:N=16 的最坏情形——朴素 union 造出一条链

8.4 聪明合并:按大小 / 按高度

union-by-size:小树挂到大树下。根数组存负的集合大小:s[root1] += s[root2]; s[root2] = root1;

union-by-height(rank):矮树挂到高树下。只有两树等高时高度才 +1——树高增长慢得多。可证:连续 M 次 union/find 总代价 (树的深度 ≤ ,因为深度 d 的节点至少有 个后代)。

白话:合并像公司并购——小公司并入大公司(按大小),或矮楼并入高楼(按高度),都防止”细长烟囱”出现。一个数组就够:根位置存负数,负数大小 = 集合大小,一举两得。

8.5 路径压缩:find 顺手修路

find 返回途中,把路径上每个节点直接改挂到根

int find(int x) {
    if (s[x] < 0)          // 根
        return x;
    return s[x] = find(s[x]);   // 递归找根,顺手把父亲改成根(路径压缩)
}

图:路径压缩——find(15) 之后整条路径全部直连根

白话:走过一遍的路全部修成高速公路——第一次 find 可能很深,之后这条线上的任何人查根都是一步直达。路径压缩会改变树高,让”按高度”失效(但”按大小”依然可靠),所以标准搭配是 union-by-rank(近似高度)+ 路径压缩

8.6 终极复杂度:O(α(N))

Tarjan 的著名结果:union-by-rank + 路径压缩,M 次操作总代价 是反阿克曼函数——增长慢到荒谬:

它是 Ω 下界匹配的:单次操作虽非严格 ,但任何由 union 和 find 组成的操作序列都不可能更快了。另外注意一个微妙结论:路径压缩单独使用(不做按秩合并)最坏是 ——两个技巧要绑定才有 界。

M 遍 vs M·log·log·… 的直觉(书中 Lemma 8.x 的递归分解): 把节点按”秩区间”分层分析压缩的收益,每层贡献一个 级别的因子,最终折叠成 。这是全书理论最深的证明之一,初学记住结论即可。

8.7 应用:迷宫生成与连通性

迷宫生成(50×88 格示例):每个格子是一个节点,与相邻格子的”墙”是待选边。不断随机选墙,若两侧格子不连通(find 不同)就拆墙 union;连通块从 个降到 1 个时迷宫完成。

图:并查集生成的迷宫

第 9 章 Kruskal 算法判定”这条边会不会成环”用的正是同一招:边的两端 find 相同 → 成环,跳过。

常见坑

只做路径压缩不做按秩:最坏 ,” 界”需要两者配合。② find 用循环实现忘了先存路径:回写父亲时要沿原路径走一遍,递归版 s[x]=find(s[x]) 一行搞定最不易错。③ 把 find 的返回值当”类名”持久化:后续 union 会改变代表元,判等价要当场比较两次 find。

通关标准

学完本篇你应该能做到:① 默写 union-by-size 与路径压缩 find 的代码(各约 5 行);② 解释为什么按秩+压缩能到 、以及为什么单用压缩不行;③ 用并查集判定无向图加边是否成环;④ 用并查集描述迷宫生成流程。