四步演进 · 从 quick-find 到 path compression
有
个对象,不断有人告知「
和
现在连在一起了」(union),需要随时回答「
和
连通吗」(connected)。这就是动态连通性 (dynamic connectivity) 问题,并查集 (union-find / disjoint-set) 是它的标准解法。本页按 Sedgewick 的经典脉络,对同一个底层数组逐步优化——从
一路降到近乎常数。四步演进依次是 Quick-Find、Quick-Union、Weighted、Path Compression。
1 · Quick-Find:直接存组号的 id 数组
最直接的设计:开一个数组 id,id[i] 存站点
属于哪一组,用组里某个站点编号当组号。那么「
和
连通吗」就是一句 id[p] === id[q]——一步
,这就是 quick-find 名字的由来。问题集中在 union:要合并两组,得把整张表里所有等于 id[p] 的格子改成 id[q],扫描一整遍
。
警示 · 用它建立一遍连通关系是
。每条 union 都要扫描全表,
个对象做
次合并就是约
次数组访问;几百万个对象时这种平方级别的访问量无法承受。根因在于 id 记的是「最终归属」而非「谁连了谁」,两组合并时被并入那组的每一个成员都得改组号,没有捷径。Quick-Union 改用森林,让 union 只改一根指针。
2 · Quick-Union:把数组读成森林
换个记法:parent[i] 不再存最终组号,只存「我的父亲是谁」。顺着 parent 一路往上走,走到指向自己的那个节点就是 root——它是这棵树(这一组)的代表。于是同一个数组现在是一片森林。union(p, q) 只要找到两边的 root,把一棵的 root 挂到另一棵 root 下,只改一个指针,再不用扫全表。
警示 · 树可能退化成一条长链。切到「最坏输入」可以看到:每次都把整棵旧树挂到新点下,树高逐步逼近
。这时 find 要从叶子一路走到顶,union 与 connected 都退化成
,不再优于 quick-find。问题出在合并时不分大小,没有控制挂接之后树会不会变高。Weighted 用一个 size 数组始终让小树挂到大树下,把树高压到
以内。
3 · Weighted:小树挂大树
quick-union 会退化,是因为合并时不区分大小,可能把高树接到了矮树下。解决方法相当简单:额外用一个 size 数组记下每棵树有多少节点,合并时始终把小树挂到大树的 root 下。这样每个节点的深度,只有在它所在的树规模翻倍时才会加一,而规模最多翻倍
次,所以树高不超过
,union 与 find 都是
。
建议 · 树高界的来由值得单独想一遍:一个节点的深度,只在它所在的树被并进一棵更大的树时才加一;而每次这种合并,新树规模至少是它原来的两倍(小的挂大的,大的不小于小的)。规模从 1 翻到
最多翻
次,所以任意节点深度不超过
。一千万个对象,树高约为 23。
已经相当好,但每次 find 仍需从某点走
步到 root——Path Compression 顺路把经过的节点全部改挂到 root。
4 · Path Compression:find 一趟压平路径
最后一项优化几乎不增加成本:find(x) 本来就要从
一路走到 root,在这一次遍历中,可以把沿途经过的每个节点都直接改挂到 root 下。这趟只多花常数代价,却让这条路径上所有点的下次 find 都一步到位。配合 Weighted 的加权合并,
次操作的摊还成本近乎常数——精确界是反 Ackermann 函数
,现实中
。
建议 ·「近乎常数」要按摊还口径读。单次操作最坏仍可能 ,但对整串操作取平均,每次成本是 。 是反 Ackermann 函数,增长极慢——即便 大到宇宙原子数量级, 也不超过 4,因此工程上可直接视为常数。加权合并配路径压缩,就是并查集的完整优化形态。
另有一种变体叫路径折半 (path halving):不想走两趟时,可在一趟里把每个节点直接指向它的祖父节点(parent[x] = parent[parent[x]]; x = parent[x];),路径也会逐步减半。效果与两趟压缩同阶,代码更短,是许多工业实现采用的版本。
5 · 四步演进总览
同一个「 个对象加 union / connected」的问题,底层数组始终不变,变的是如何解读它、合并时挂在哪边、查询时是否顺带整理。
| 实现 | 初始化 | union | connected / find | 关键 trick |
|---|---|---|---|---|
| quick-find | N | N | 1 | id 数组直接存组号 |
| quick-union | N | 树高 | 树高(最坏 N) | parent 数组构成森林 |
| weighted QU | N | lg N | lg N | 小树挂大树 |
| weighted + 压缩 | N | ~1 | ~1 | find 时压平路径 |
它的落地场景相当广:Kruskal 最小生成树判环(MST · Kruskal 里那段 DSU 即是其应用)、网络与电路连通性、变量引用等价归并、社交网络好友关系归并、图像连通域标记、离线连通查询、编译器的 union 类型等价判定。凡是「不断合并、随时查询是否同组」的场景,大多适用。
并查集的森林是「只向上指 parent、不维护孩子」的极简树,与最小生成树的 Kruskal 紧密配合;它的近乎常数摊还依赖于把一次昂贵操作的收益分摊给后续操作,这种摊还视角与区间查询里各种预处理结构思路相通。
相关链接
- 本仓库 · MST Kruskal 中的 union-find 应用 /mst 并查集在 Kruskal 判 cycle 中的应用:两端 find 相同 ⟹ 加边成环 ⟹ 丢弃。本系列是该应用的原理详解。
- Union-Find (Algorithms, 4th ed. §1.5) algs4.cs.princeton.edu Sedgewick & Wayne 的权威教材:dynamic connectivity 的提出、quick-find → quick-union → weighted → 路径压缩的完整演进与摊还分析。本系列即按此脉络组织。
- Union-Find Data Structure (交互可视化) observablehq.com Bryan Gin-ge Chen 的 Observable notebook:可拖拽、可单步的并查集动画,直观感受各版本树形差异。
- Disjoint-set data structure Wikipedia 并查集的正式定义、union by rank / size、路径压缩,以及反 Ackermann 函数 α(N) 摊还界的来历 (Tarjan)。
- Union-Find 算法详解 (中文译介) blog.csdn.net dm_vincent 对 Sedgewick union-find 一节的中文整理,四个版本的代码与复杂度对照,本系列参考来源之一。