并查集很擅长回答“现在是不是一伙”,却有一点健忘:所有人最终合并后,它不记得谁先握手、谁后入群。Kruskal 重构树的想法像给每次合并拍一张合影,并把合影按发生顺序叠成一棵树。
学完后,你能:
前置:无向带权图、Kruskal、并查集、树的祖先概念。LCA 的倍增实现可留到正式课。
用 1 到 5 五张人物卡和下列边卡:
(1,2,2) (3,4,3) (2,3,5) (4,5,6) (1,5,9)
边卡第三项是时间/权值。纸上画四栏:对象(当前有哪些群)、状态(每群的代表)、动作(拿起哪张边卡)、账本(本次真的把哪两群合并、时间是多少)。先别画一棵大树,先让 DSU 的健忘症露面。
从小到大处理卡片。每次只问:两端现在在同一个泡泡里吗?若不在,就把两个泡泡画成一个更大的泡泡。
| 当前边 | 两端处理前的泡泡 | 选不选 | 处理后的泡泡 |
|---|---|---|---|
| (1,2,2) | |||
| (3,4,3) | |||
| (2,3,5) | |||
| (4,5,6) | |||
| (1,5,9) |
停顿 0:处理到权值 5 后,1 与 4 是否连通?处理到权值 3 后呢?请不要看最终结果再倒推。
现在问:2 与 4 是在何时首次连通?如果你只保存普通 DSU 的最后父数组,所有叶子可能都指向同一代表;若开着路径压缩,连“先经过谁”也看不到。重跑一遍当然能回答一个询问,很多询问就会把相同历史反复回放。
停顿 1:在处理权值 6 后,若只留最终集合 {1,2,3,4,5},你还区分得出 2-4 的首次连通时间和 1-5 的首次连通时间吗?缺的究竟是一个数,还是一段结构?
第一次成功合并 1、2 时,拿一张新事件卡 6,写上权值 2;让它的孩子是原点 1、2。DSU 的新代表也改成 6。第二次成功合并 3、4,拿事件卡 7,写上 3。第三次合并两个大泡泡,拿事件卡 8,写上 5,孩子正是 6、7。最后拿事件卡 9,写上 6,孩子是 8、原点 5。
停顿 2:节点 8 的两个孩子为什么必须是 6、7,而不能直接接 1、2、3、4 四个叶子?若直接接四个叶子,丢失了哪两次发生在更早时刻的合并?
在刚画的树中,2 的祖先链是 2→6→8→9,4 的是 4→7→8→9。第一次相同的祖先是 8,卡上写 5。因此:
$$
\operatorname{value}(\operatorname{LCA}(u,v)). $$
不是因为 LCA 有魔法,而是任何时刻 T 对应“删掉所有 value>T 的祖先连接”。u、v 在同一块,当且仅当它们已共同落在某个 value≤T 的事件节点下;最小这样的 T 就是它们最低的共同祖先权值。
若有 q 个问题,每个都问“两点在多大阈值后连通”或“阈值 T 时点 u 的连通块多大”,重跑 Kruskal 是 O(qm log m) 量级的重复劳动。重构树把所有阈值的合并历史一次性展开为至多 2n-1 个节点;随后用 LCA、祖先跳跃或子树信息查询。
本节按边权递增处理。新事件一定发生在两个旧事件之后,所以从叶到根,节点权不减。对阈值 T,点 u 所在的“当前块”是 u 向上走、但不跨过 value>T 的最高祖先。它的叶子数可预先存在该祖先的 subtree_size 中。
build_reconstruction_tree(edges in nondecreasing value):
make every original vertex a DSU root and a leaf
for (u, v, w) in edges:
a = find(u); b = find(v)
if a == b: continue
event = new_node(value=w, children=[a, b])
DSU_parent[a] = event
DSU_parent[b] = event
DSU_parent[event] = event
这里的 DSU 根不再一定是原顶点,它是“当前连通块在重构树中的根”。这正是实现中最容易写错的身份切换。
若按边权从大到小合并,内部节点权从叶到根不增。它适合问“只保留权值至少 W 的边,两点是否已连通”或最大瓶颈路径。不要机械套递增版的比较号:先在纸上写清 T 是“允许不超过”还是“允许至少”,再决定向祖先跳的条件。
停顿 3:若从大到小处理边,两个点的 LCA 权值 8,表示它们在保留权值至少多少的边时开始连通?阈值往更低还是更高移动会让连通块变大?
| 账本 | 存什么 | 为什么不能省 |
|---|---|---|
| DSU | 当前每个连通块的重构树根 | 快速判断这条边是否带来新合并 |
| 重构树 | 每次成功合并的两个旧根与权值 | 保留所有历史阈值 |
| 树上预处理 | parent、depth、up、subtree_size | 快速求 LCA、跳到阈值位置 |
树最多有 n 个叶与 n-1 个成功合并节点;若原图不连通,会得到森林,常加一个虚根或按题目逐树处理。建立阶段排序 O(m log m),DSU O(m α(n));LCA 预处理 O(n log n),每次两点阈值查询通常 O(log n)。
在一棵 MST 上,u 到 v 路径的最大边权也等于递增重构树 LCA 的权值:Kruskal 在该时刻恰把二者连起来。这是“最小瓶颈路径”与重构树相遇的地方;但不要反过来以为重构树本身就是原 MST,它记录的是合并关系,不保留被跳过的所有原边。
请把下表补完,再画 6、7、8、9:
| 新节点 | 发生权值 | 左孩子 | 右孩子 | 覆盖的原点 |
|---|---|---|---|---|
| 6 | ||||
| 7 | ||||
| 8 | ||||
| 9 |
再设阈值 T=3、5、6,分别圈出点 2 所属的最高允许祖先。你应该得到 {1,2}、{1,2,3,4}、{1,2,3,4,5}。这就是“向上跳到不超过 T 的最高事件”的具体含义。
运行 把合并史写成树.py。它先让你预测 2 与 4 何时相遇,再逐条打印 Kruskal 边卡与新事件节点。可输入:
先自己画祖先链,再让程序做 LCA;不要把“它输出 5”误当成已经理解了为什么是 5。