城市想全部连通,预算却嫌每条路都贵。今天的原则很克制:能让两个已经连通的地方再绕一圈的路,先别修;能把两块大陆第一次连起来的便宜路,很值得认真看一眼。
学完后,你能:
find/union。前置:无向带权图、排序、连通性。
请把边写成卡片 (端点,价格),把每个当前连通块圈成一个泡泡。每翻一张边卡,都在纸上回答四件事:它两端在哪两个泡泡、加它会不会闭环、若加入泡泡如何合并、为什么可以暂时相信这条边。不要一上来就把边按算法名分类。
今天的目标是亲自搭出“森林 → 连通块 → 割 → 安全边”的框架;并查集只是这份账本的高效记事员。
画四个点和五条无向边:A-B(1), A-C(2), B-C(3), C-D(2), B-D(4)。先不要排序;任选几组边,检查它们是否让所有点连通。每组都数一数边数和是否含环。
停顿 0:若一个连通方案有 4 个点却选了 4 条边,它一定含环吗?删掉环上一条边后,连通性为什么不会消失?
从四个单点泡泡 {A},{B},{C},{D} 开始,按价格查看边卡。先看 A-B(1):它连接两个不同泡泡;再看 A-C(2);随后看 C-D(2);最后再看 B-C(3)。
请填:
| 正在看的边 | 两端当前泡泡 | 加入/跳过 | 加入后泡泡 | 理由 |
|---|---|---|---|---|
| A-B(1) | ||||
| A-C(2) | ||||
| C-D(2) | ||||
| B-C(3) |
停顿 1:到
B-C(3)时,为什么不需要真的在图上 DFS 寻路,也能知道它会成环?
把当前泡泡 $S={A,B}$ 用一条虚线和外界隔开。跨线边有 A-C(2), B-C(3), B-D(4)。请先选择最轻跨线边;再假设某棵最优树没有它,沿那棵树从 S 走向外部时,找第一条跨虚线的边。
停顿 2:把最轻边加进那棵最优树后为什么出现环?环上为什么一定有另一条跨虚线的边可以删?
给 A,B,C,D 各写一个 parent,初始都指向自己。依序 union(A,B),union(A,C),union(C,D),每步跟父亲指针到根。然后再做 find(B),find(C)。
停顿 3:
find的返回值到底代表一条路径,还是一个集合的名字?路径压缩改的是正确性还是让下次更快?
请补全:状态是 ______;候选按 ______ 顺序来;若两端 ______ 就选;选后 ______;停止条件是 ______。
一张图有 $m$ 条边,枚举“选或不选”已有 $2^m$ 种子集;还要逐份检查是否连通、是否无环。我们需要一种不回头的大原则:每次只看一条当前最便宜的边,若它连接两个不同连通块,就加入;若两端早已在同一块,加入只会造环,跳过。
这就是 Kruskal。它不像是在“猜整棵树”,而是在慢慢把很多孤岛焊成一个整体。
若一套连通方案含一个环,删掉环上任意一条边,环的另外一侧仍让两端连通;所以至少有一条边是多余的。最优方案当然不会为纯粹的绕圈付钱,因而最小生成树一定无环。
反过来,只要当前森林的两端属于不同连通块,加入一条边不会形成环,并恰好把两个块并成一个。这个判定不需要真的画完整路径;DSU 维护每个点所属的代表元即可。
sort edges by weight ascending
for (u, v, w) in edges:
if find(u) != find(v):
choose (u, v, w)
union(u, v)
else:
skip it # 它会在当前森林中闭合一个环
stop after choosing n - 1 edges
把某个当前连通块 $S$ 和外面切开。Kruskal 正在看的边 $e$ 是尚未考虑边中最轻、又跨过这个割的一条。取任意一棵最小生成树:若它已有 $e$,很好;若没有,加入 $e$ 会出现一个环,环上必有另一条也跨这个割的边 $f$。因为 $e$ 不比 $f$ 重,删 $f$ 换 $e$ 不会让总权变大。
这叫割性质。注意它说“至少存在一棵 MST 可选这条最轻边”;同权时树的形状可能不唯一,但最小总权不受影响。
DSU 的每个集合有一个根。find(x) 沿父亲指针找到根,并把路上节点直接挂到根(路径压缩);union(a,b) 把较小的树挂到较大的树(按大小/秩合并)。它不保存具体路径,专职快速回答“两个点目前是否已经连通”。
find(x):
if parent[x] == x: return x
parent[x] = find(parent[x])
return parent[x]
union(a, b):
ra, rb = find(a), find(b)
if ra == rb: return false
attach smaller root below larger root
return true
对边表 A-B(1),D-E(1),A-C(2),C-D(2),B-C(3),B-D(4),C-E(5),画五个单点泡泡,按权执行:
| 边 | find(u),find(v) |
选/跳 | 当前已选边 | 分量 |
|---|---|---|---|---|
| A-B(1) | ||||
| D-E(1) | ||||
| A-C(2) | ||||
| C-D(2) | ||||
| B-C(3) |
排序是 $O(m\log m)$,DSU 的总工作近似 $O(m\alpha(n))$,其中 $\alpha$ 小得像竞赛时限里的一粒芝麻;所以整体由排序主导。
int。运行 只修够用的路.py。直接回车会使用 5 个城市的预设图。每条边出现时,程序会打印两个端点所在分量、是否选中、若跳过则展示它闭合的环意图。
可以输入自己的 A-B-3,B-C-1 边表;先猜哪条边会因为“已经连通”被拒绝,再看 DSU 的回答。
Prim 是从一个已选点集向外找最轻边,背后仍是割性质;“建超级源”可把建站费与连线费统一进一张图。正式课会讨论隐式完全图、替换边和 MST 边分类——它们都要求先把今天这个安全交换说清楚。
find(u)==find(v) 时加入边一定成环?find/union 的父亲数组。