Article
最小生成树 (Minimum Spanning Trees, MST)
算法导论-CH21-最小生成树,待补充摘要。
最小生成树 (Minimum Spanning Trees, MST)
一、 图算法预热问题:无向图的环判定 (Cycle Detection)
【问题定义】:给定一个无向图 ,如何判断该图中是否存在环?
方法 1:基于深度优先搜索 (DFS)
- 核心思想: 从任意未访问的顶点开始进行 DFS。在搜索过程中,如果遇到一个已被标记(marked)的邻接顶点,说明存在环。
- 潜在陷阱与解决方案:
- 陷阱:无向图中的边是双向的。当从顶点 访问到 时, 的邻居中必然包含 。如果直接检查已标记的邻居,会误将“走回头路(回到父节点)”判定为环。
- 解决方案:在 DFS 递归调用中传入当前节点的父节点(即来源节点),不将父节点视为成环的标记点。
- 时间复杂度:
- 最坏情况:。
- 优化分析:对于无向图,一旦检查的边数超过 ,根据树的性质,图中必然已经包含环。因此,我们可以限制检查的边数,从而将最坏情况时间复杂度收紧为 。
方法 2:基于加权并查集 (Weighted Quick Union with Path Compression, WQUPC)
- 核心思想: 遍历图中的每一条边 :
- 调用
connected(v, w)检查 和 是否已经连通。 - 如果 不连通,说明当前边不会构成环,调用
union(v, w)将它们合并到同一个连通分量中。 - 如果 已连通,说明在添加这条边之前, 和 之间就已经存在一条通路,现在加上这条边必然会构成一个环,直接返回
true(存在环)。
- 调用
- 时间复杂度:
- 最坏情况:包含路径压缩时,每次 Union-Find 操作耗时为 (其中 是反阿克曼函数,增长极其缓慢,在实际中可视为常数)。
- 遍历所有边:。同样,因为一旦加入第 条边必然成环,所以实际最多进行 次
union操作,总时间复杂度可以简化为 。
二、 最小生成树 (MST) 的概念与割性质 (Cut Property)
1. 生成树 (Spanning Tree) 的定义
给定一个无向连通图 ,其生成树 是 的一个子图,且必须满足以下三个条件:
- 连通性:包含图中的所有顶点。
- 无环性(Acyclic):不包含任何环。
- 顶点全覆盖:包含全部 个顶点,且只有 条边。
最小生成树 (MST):在所有可能的生成树中,边权值之和最小的生成树。 注:若图中的边权值均不相同,则该图的 MST 是唯一的。为了讨论简便,后续均默认边权值唯一。
2. 割性质 (Cut Property) — 寻找 MST 的基石
- 割 (Cut):将图中的顶点划分为两个非空、互不相交的集合(例如灰色顶点集合与白色顶点集合)。
- 横跨边 (Crossing Edge):一条端点分别属于这两个不同集合的边。
割性质的数学证明(反证法)
- 假设 是图 的 MST,但权值最小的横跨边 不在 中。
- 如果我们将 添加到 中,由于 是生成树,加入一条边必然会形成一个唯一的环。
- 这个环中必定存在另外一条横跨该割的边 (因为环要跨过去再跨回来才能闭合)。
- 显然,边 的权值 (因为 是所有横跨边中权值最小的)。
- 如果我们用 替换 (即令 ),得到的 仍然是一棵连通且无环的生成树,但其总权重 。
- 这与 是 最小 生成树的假设产生矛盾!因此,最小横跨边 必须包含在 MST 中。
💡 割性质应用例题
【题目】:已知割集合为 (如下图中的紫色顶点),剩余顶点 为另一侧。给定以下边权值列表,请找出哪条边属于该割的最小权重横跨边(必在 MST 中)?

【解析过程】:
-
首先,识别出所有一端在 ,另一端在 的 横跨边:
- 的外部邻居: (0.26), (0.34), (0.36)
- 的外部邻居: (0.29)
- 的外部邻居: (0.28), (0.32), (0.35)
- 的外部邻居: (0.58), (0.93) (注:像 2-3、3-6 这样两端都在同一颜色集合内部的边不是横跨边)
-
在所有找到的横跨边中,比较它们的权值:
-
其中权值最小的是边 (权值为 0.26)。
-
【结论】:根据割性质,边 必定在 MST 中。
3. 通用 MST 寻找算法 (SOP)
基于割性质,我们可以设计出寻找 MST 的通用框架:
- 起初,MST 的边集为空。
- 寻找一个割,使得该割的所有横跨边中,目前没有一条已经被选入 MST。
- 找到该割中 权重最小的横跨边,将其加入 MST 边集。
- 重复上述步骤,直到 MST 中包含了 条边。
三、 Prim 算法(普里姆算法)
1. 概念化设计 (Conceptual / Ideal)
- 思想:从一个任意的起始顶点 开始,将其放入已经构建的树中。在每一步中,寻找有一个端点在树内,另一个端点在树外的所有边(即以“当前树的顶点集合”和“其余顶点集合”作为割),选择其中权重最小的边,将树外的那个邻接点及该边并入树中。
- 终止条件:重复此步骤,直到树中包含了所有 个顶点(即合入了 条边)。
2. 高效实现设计 (Realistic / Optimized)
如果每次都去遍历所有横跨边,效率会非常低下(如你手写笔记所说:“Iterating over all magenta edges is unnecessary and slow”)。
- 解决方案:引入优先队列 (PQ, Fringe)。
- 队列中的元素为:顶点 以及当前已知的 到正在构建的 MST 的最短距离 (
distTo[v])。 - 每次从优先队列中取出
distTo最小的顶点 (即离当前 MST 最近的未访问点)。 - 接着,进行松弛操作 (Relaxation):遍历 的所有邻接边 ,如果发现边 的权重比 当前记录的
distTo[w]还要小,则更新distTo[w] = e.weight()并将edgeTo[w] = e,同时降低其在 PQ 中的优先级。
- 队列中的元素为:顶点 以及当前已知的 到正在构建的 MST 的最短距离 (
🌟 经典对比:Prim vs. Dijkstra
| 维度 | Dijkstra 算法 | Prim 算法 |
|---|---|---|
| 关注核心 | 寻找单源最短路径树 (SPT) | 寻找最小生成树 (MST) |
| 访问顺序 | 顶点按照到源点 的总路径累加距离从小到大被访问 | 顶点按照到当前 MST 整体的最近距离从小到大被访问 |
| 松弛机制 (Relaxation) | distTo[w] > distTo[v] + e.weight() | distTo[w] > e.weight() |
| 边权值要求 | 不能处理含有负权值的图(会失效) | 可以正确处理含有负权值的图(只要无负环) |
❓ 思考题:MST 也是某一顶点的 SPT 吗?
【问题】:对于一个给定的图,其 MST 是否也必然是某一个特定起点下的 SPT? 【解答】:不一定。 如下图所示:
B —— 1 —— C
/ \
2 2
/ \
A —— 2 —— D —— 2 —— s
其 MST 必须选择权重最小的边。如果我们选择底部左侧的起点,在生成 SPT 时为了使到各点的绝对距离最短,会引入更多的 2 权重边,而 MST 只会保留必需的 条边。因此,SPT 严重依赖于起点的选择,而 MST 关注的是整体连接成本的最优化,两者并不等价。
3. 代码实现 (Java)
根据课件和手写笔记要求,以下为 Prim 算法的完整 Java 实现:
import java.util.ArrayList;
import java.util.List;
public class PrimMST {
private Edge[] edgeTo; // edgeTo[v] = 离当前 MST 最近的边
private double[] distTo; // distTo[v] = 顶点 v 到当前 MST 的最小距离
private boolean[] marked; // marked[v] = true 表示 v 已经在 MST 中
private IndexMinPQ<Double> pq; // 索引优先队列,用于动态获取最近的顶点
public PrimMST(EdgeWeightedGraph G) {
edgeTo = new Edge[G.V()];
distTo = new double[G.V()];
marked = new boolean[G.V()];
pq = new IndexMinPQ<Double>(G.V());
// 初始化所有顶点的距离为无穷大
for (int v = 0; v < G.V(); v++) {
distTo[v] = Double.POSITIVE_INFINITY;
}
// 从顶点 0 开始构建 MST
distTo[0] = 0.0;
pq.insert(0, 0.0);
while (!pq.isEmpty()) {
int v = pq.delMin(); // 取出离 MST 最近的顶点
scan(G, v);
}
}
// 松弛与该顶点相连的所有边
private void scan(EdgeWeightedGraph G, int v) {
marked[v] = true; // 将该顶点正式合入 MST
for (Edge e : G.adj(v)) {
int w = e.other(v);
if (marked[w]) continue; // 邻接点已在 MST 中,跳过(防止成环)
if (e.weight() < distTo[w]) {
distTo[w] = e.weight(); // 更新 w 到当前树的最近距离
edgeTo[w] = e;
if (pq.contains(w)) {
pq.decreaseKey(w, distTo[w]); // 降低优先级
} else {
pq.insert(w, distTo[w]);
}
}
}
}
// 获取 MST 中的所有边
public Iterable<Edge> edges() {
List<Edge> mst = new ArrayList<>();
for (int v = 0; v < edgeTo.length; v++) {
if (edgeTo[v] != null) {
mst.add(edgeTo[v]);
}
}
return mst;
}
}
4. Prim 算法时间复杂度推导
为什么使用二叉堆(Binary Heap)实现优先队列时,Prim 算法的运行时间是 ?
我们将 Prim 算法的每个操作拆解分析:
| 操作步骤 | 执行次数 | 单次时间复杂度 | 总时间开销 |
|---|---|---|---|
PQ 插入顶点 (insert) | |||
取出最小顶点 (delMin) | |||
降低优先级 (decreaseKey) | 最坏情况 |
-
总开销之和:
-
简化分析: 在连通图(Connected Graph)中,边的数量一定满足 。因此 。 由此可得,总时间复杂度由最大的项决定,即:
四、 Kruskal 算法(克鲁斯卡尔算法)
1. 概念化设计 (Conceptual / Ideal)
- 核心思想: 不要限制在“从单个顶点开始向外延展”。我们将图中的所有边看作一个整体:
- 将图中的所有边按照权值从小到大进行排序。
- 依次选择当前权值最小的边。
- 检查:加入这条边是否会在已选的 MST 边集中构成环?
- 若 不会构环:正式将该边加入 MST。
- 若 会构环:舍弃该边。
- 重复此操作,直到选出了 条边。
2. 优化实现设计:如何解决“每次判断是否有环开销太大”的问题?
💡 纠正手写笔记中的疑惑: > 你在笔记中写道:“每次都判断是否有环,开销是不是太大了?” 解答:如果使用常规的 DFS 查找环,每次查环的开销为 ,对于所有边总开销会飙升至 ,开销确实极其庞大。 为了彻底解决这个性能瓶颈,我们引入了**并查集(Union-Find)**数据结构!
- 并查集是如何工作的?
- 在并查集中,初始时每个顶点都是一个独立的连通分量(集合)。
- 当我们要考虑一条边 时:
- 仅需要调用
uf.connected(v, w)来查询 和 是否处于同一个连通分量中。 - 如果不在同一个集合中,说明此时加入这条边绝对不会产生环,紧接着调用
uf.union(v, w)将两者的集合合并。 - 如果已经在同一个集合中,说明加入此边会产生环,直接抛弃。
- 仅需要调用
- 利用带有路径压缩和按秩合并的加权并查集,
connected和union操作的单次时间复杂度仅为几乎常数的 。这使得成环检测几乎“免费”,彻底解决了性能瓶颈!
3. 代码实现 (Java)
根据手写笔记要求,Kruskal 算法的经典实现如下:
import java.util.ArrayList;
import java.util.List;
public class KruskalMST {
private List<Edge> mst = new ArrayList<Edge>(); // 存储最终 MST 的边集
public KruskalMST(EdgeWeightedGraph G) {
// 1. 将所有边插入一个最小优先队列中以实现自动排序
MinPQ<Edge> pq = new MinPQ<Edge>();
for (Edge e : G.edges()) {
pq.insert(e);
}
// 2. 初始化并查集,初始时每个顶点都是独立集合
WeightedQuickUnionPC uf = new WeightedQuickUnionPC(G.V());
// 3. 循环获取最小边,直到队列为空或已选够 V-1 条边
while (!pq.isEmpty() && mst.size() < G.V() - 1) {
Edge e = pq.delMin(); // 取出权值最小的边
int v = e.from();
int w = e.to();
// 利用并查集判断 v 和 w 是否已连通(即加入该边是否会成环)
if (!uf.connected(v, w)) {
uf.union(v, w); // 合并两个不相交集合
mst.add(e); // 将该边安全加入 MST
}
}
}
public Iterable<Edge> edges() {
return mst;
}
}
4. 彻底攻克:Kruskal 算法的 Runtime 计算与推导
为了让你完全看懂 Kruskal 的时间复杂度计算,我们将所有核心操作和其调用次数进行逐步拆解。
场景 A:使用最小优先队列(Min-PQ)的一般情况
假设图中有 个顶点, 条边,我们采用二叉堆(Binary Heap)实现 Min-PQ。
| 操作步骤 | 调用次数 | 单次时间复杂度 | 总时间复杂度 |
|---|---|---|---|
将所有边插入 PQ (insert) | 次 | ||
取出最小边 (delMin) | 最坏 次 | ||
并查集连通判断 (connected) | 最坏 次 | ||
并查集合并 (union) | 恰好 次 |
-
整体累加复杂度:
因为 的增长速度远快于具有反阿克曼尺度的 ,所以总复杂度由 PQ 的操作占主导,即:
-
🎉 关键数学转换:为什么 ? 由于图中任意两点间最多只有一条边,所以最大边数限制为 。 我们将 带入 中进行缩放:
根据大 表示法的常数忽略性质, 在渐进意义上等价于 。因此:
(这说明在最坏情况下,Kruskal 算法和 Prim 算法在渐进时间复杂度上是完全一致的。)
场景 B:高级优化技巧(Heapification 与预排序)
1. 利用“自底向上堆化(Bottom-up Heapification)”优化 PQ 初始化
- 原理解析:在 HeapSort 中我们会学到,若将一个无序的包含 个元素的数组构建成堆,如果逐个调用
insert,开销是 。但如果直接使用“下沉松弛(Sink-based heapify)”,可以将建堆的时间大幅缩短至 。 - 对整体复杂度的影响:虽然初始化变快成了 ,但我们后续仍需进行最多 次
delMin操作(每次开销 ),因此总复杂度依然为 。
2. 预排序边集(Pre-sorted Edges)的最极致优化情况
-
原理解析:如果图中的边在输入时就已经排好序,或者我们可以使用基数排序(Radix Sort,在某些特定整数边权下可用)在 时间内排好序:
- 我们将不再需要 PQ,而是直接使用普通循环遍历已排好序的数组(每次取最小边只需 的时间,共进行 次,总共 )。
- 此时,算法的所有耗时将彻底由 Union-Find 统治!
-
此时的总复杂度推导:
由于 在宇宙范围内其值都不超过 5,所以此时的 Kruskal 算法可以说是达到了惊人的近乎线性时间复杂度!
五、 最短路径与 MST 算法终极对比表
| 算法 | 解决问题类型 | 时间复杂度 (若 ) | 适用条件与备注说明 |
|---|---|---|---|
| Dijkstra 算法 | 单源最短路径树 (SPT) | 不能处理存在负权重边的图。 | |
| Prim 算法 | 最小生成树 (MST) | 适合稠密图,逻辑与 Dijkstra 几乎完全一致。 | |
| Kruskal 算法 | 最小生成树 (MST) | 适合稀疏图,利用 WQUPC 实现极其高效的成环检测。 | |
| Kruskal 算法 (预排序边) | 最小生成树 (MST) | 极速版。适用于边权已排序或可线性时间排序的场景。 |
CLRS 第 21 章:最小生成树 (MST) 深度巩固练习指南
本指南紧扣你上传的《算法导论》第 21 章教材,挑选了最经典的课后习题与思考题。这些题目将帮助你深入理解割性质 (Cut Property)、环性质 (Cycle Property) 以及 Prim/Kruskal 算法的边界条件。
目录
第一阶段:割性质与 MST 基本理论 (21.1 节)
这一节的习题旨在帮你巩固割性质的证明逻辑,并引入另一个极度对称的重要性质——环性质 (Cycle Property)。
✍️ 练习 21.1-1 (经典结论证明)
-
英文原题:
Let be a connected, undirected graph with a real-valued weight function defined on . Let be a subset of that is included in some minimum spanning tree for , let be any cut of that respects , and let be a light edge crossing . Show that is safe for .
-
中文翻译: 设 为一个带权无向连通图。设 是 的一个子集,且 包含在 的某棵最小生成树中。设 是 的任意一个不破坏 (即 中没有边横跨该割)的割,且 是横跨该割的一条轻边(即权重最小的横跨边)。证明:边 对于集合 是安全的(即 依然包含在某棵 MST 中)。
-
核心考点: 割性质定理的严格证明(反证法与替换法)。
-
解题思路提示 (Hint):
- 设 是包含 的一棵 MST。如果 ,则结论显然成立。
- 如果 ,那么将 加入 会形成一个包含 的唯一环。
- 因为 且 ,这个环中必然存在另一条横跨 的边 。
- 考虑构造一棵新树 ,证明 ,从而得出 也是 MST 的结论。
【证明步骤】:
-
假设包含 的 MST 为 。若 ,则 ,边 显然安全。
-
若 ,由于 是生成树,我们将 加入 中,在 中必然包含一个唯一的环 。
-
由于 且 ,环 必须跨越割 至少两次才能闭合。因此,在环 上必然存在另一条横跨该割的边 。
-
因为割 尊重 ,所以横跨边 绝对不属于 (因为 中没有横跨边)。因此 。
-
因为 是横跨割 的轻边(最小权重边),所以有:
-
我们构造一棵新树 。
-
连通性与无环性:由于我们从环中删去了一条边 并加入了一条同样能连接两个分量的边 , 依然是一棵合法的生成树。
-
权重对比:
因为 ,所以有 。
-
-
又因为 已经是 MST(权重最小),所以 只能等于 。这意味着 也是一棵 MST。
-
显然,。因此, 对于 是安全的。
✍️ 练习 21.1-5 (环性质 Cycle Property)
-
英文原题:
Let be a maximum-weight edge on some cycle in . Prove that there is a minimum spanning tree of that is also a minimum spanning tree of . That is, there exists a minimum spanning tree of that does not include .
-
中文翻译: 设 是图 中某个环 上权重最大的边。证明:图 的最小生成树同时也是 的最小生成树。换句话说,存在一棵不包含边 的图 的最小生成树。
-
核心考点: 环性质 (Cycle Property)。这是与割性质齐名的图性质:在任意一个环中,权重严格最大的边一定不在 MST 中。
-
解题思路提示 (Hint):
- 设 是 的任意一棵包含 的 MST。
- 如果我们把 从 中删去, 会分裂成两个不连通的连通分量 和 。
- 因为 原本处于环 上,环上的其他边必然可以提供另一条通路连接 和 。
- 找到环 上除 之外的另一条横跨 和 的边 ,比较 和 。
【证明步骤】:
-
假设 是 的一棵包含边 的 MST。若能证明存在另一棵权重相同的 MST 不包含 ,则命题成立。
-
从 中移除边 。此时 被分割为两棵不相交的子树 与 。这在顶点集上隐式地定义了一个割 ,其中 覆盖 , 覆盖 。
-
边 是横跨割 的一条边。
-
既然 属于环 ,如果我们沿着环 从 的一个端点出发走到另一个端点,路径上必然存在另一条边 横跨割 (因为环必须从 跨到 再跨回来)。
-
题目指出 是环 上权重最大的边,所以必有:
-
我们构造新图 :
-
连接了所有的顶点且无环,是一棵生成树。
-
它的总权重为:
-
因为 ,所以 。
-
-
因为 已经是 MST,所以必有 。因此 也是 的一棵最小生成树。
-
由于 不包含边 ,它同时也是移除了边 之后的子图 的一棵 MST。
✍️ 练习 21.1-8 (权值单调递增变换)
-
英文原题:
Let be a minimum spanning tree of a graph , and let be a subset of . Let be the subgraph of induced by . Is necessarily a minimum spanning tree of the subgraph of induced by ? If so, prove it; otherwise, give a counterexample.
-
中文翻译: 设 是图 的一棵最小生成树,且 是 的一个子集。设 是由 在 中导出的子图。 是否一定是由 在 中导出的子图 的最小生成树?如果是,请予以证明;如果不是,请举出一个反例。
-
核心考点: 子图诱导(Induced Subgraph)下的 MST 保持性问题。
-
解题思路提示 (Hint):
- 试着画一个极其简单的图,比如三个顶点呈线性或环状排列。
- 例如: 权重分别为 1 和 2,同时存在一条 权重为 3 的边。
- 选择一个顶点子集 。看看在原树 中,它们之间是否还有边?在原图中,它们是否有直接相连的更短的边?
【解答】:不一定(结论不成立)。
【反例说明】:
- 设无向图 包含 3 个顶点 ,边集及权重如下:
- 显而易见, 的唯一最小生成树 包含边 ,总权重为 2。
- 现在我们取顶点子集 。
- 那么由 导出的原图子图 包含顶点 以及它们之间的边 (权重为 3)。因此 的 MST 显然必须包含边 。
- 然而,由 在树 中导出的子图 ,由于 中没有直接连接 和 的边(因为在 中它们是通过 间接相连的,而 ),所以子图 根本不包含任何边。
- 一个没有边的子图 显然连连通都做不到,更不可能是 的最小生成树。
【追加思考】:如果题目改成“把 分裂成两个由割定义的子图”,结论会如何?在学习图算法时,一定要小心这类“局部最优”不等于“全局诱导最优”的陷阱。
第二阶段:Kruskal 与 Prim 算法执行细节 (21.2 节)
这部分题目针对具体算法的运行步骤和边界场景,能切实检验你是否掌握了两个算法的底层逻辑。
✍️ 练习 21.2-1 & 21.2-2 (算法执行过程追踪)
- 英文原题意图: 给定一个具体的带权图(通常是 CLRS 图 21.4,包含 9 个顶点 到 ),要求分别写出 Kruskal 和 Prim 算法加入边的顺序。
- 练习建议: 由于你手写笔记中记录了 Prim 和 Kruskal 的 Demo 逻辑,请用以下这个经典的 5 点图做一次手算演练。
[B]
/ \
3 1
/ \
[A]—— 4 ——[C]
\ /
5 2
\ /
[D]
【任务 1:Kruskal 算法演练】 请写出边排序后的列表,并依次判断是否加入,记录并查集的变化过程。
【任务 2:Prim 算法演练(从 A 出发)】 请记录优先队列(Fringe)中每个顶点的 distTo 变化,以及每次被 delMin 取出的顶点。
任务 1 (Kruskal) 步骤拆解:
- 边按权重排序:
- (1)
- (2)
- (3)
- (4)
- (5)
- 依次处理:
- 取出 (1):并查集连通性检查: 不连通。加入 MST,合并 。
- 取出 (2):并查集连通性检查: 不连通。加入 MST,合并 。
- 取出 (3):并查集连通性检查: 不连通。加入 MST,合并 。
- 此时已加入 条边,算法可以提前结束。
- 最终 MST 边集:,总权重为 6。
任务 2 (Prim 从 A 出发) 步骤拆解:
- 初始化:
distTo[A] = 0, 其它为 。- Fringe PQ:
[(A:0), (B:∞), (C:∞), (D:∞)]
- 第 1 轮:
delMin()取出 。标记 已访问。- 松弛 的邻居:
- : 更新
distTo[B] = 3 - : 更新
distTo[C] = 4 - : 更新
distTo[D] = 5
- : 更新
- Fringe PQ:
[(B:3), (C:4), (D:5)]
- 第 2 轮:
delMin()取出 。标记 已访问。- 松弛 的邻居中未访问的点( 和 ):
- : (原
distTo[C]) 更新distTo[C] = 1,edgeTo[C] = B-C
- : (原
- Fringe PQ:
[(C:1), (D:5)]
- 第 3 轮:
delMin()取出 。标记 已访问。- 松弛 的邻居中未访问的点(仅剩 ):
- : (原
distTo[D]) 更新distTo[D] = 2,edgeTo[D] = C-D
- : (原
- Fringe PQ:
[(D:2)]
- 第 4 轮:
delMin()取出 。标记 已访问。Fringe 为空,结束。
- 最终 MST 边集:。
✍️ 练习 21.2-3 (当边权是小范围整数时的优化)
-
英文原题:
Suppose that all edge weights in a graph are integers in the range from to . How fast can you make Kruskal’s algorithm run? What if the edge weights are integers in the range from to some constant ?
-
中文翻译: 假设图 中所有边的权重都是介于 到 之间的整数。你能将 Kruskal 算法的运行时间优化到多快?如果边的权重是介于 到某个常数 之间的整数,结果又会如何?
-
核心考点: 你在手写笔记里提到的“预排序情况下的 Kruskal 复杂度推导”。这里考查利用非比较排序(如计数排序/基数排序)替代二叉堆来突破 的限制。
-
解题思路提示 (Hint):
- Kruskal 算法的传统瓶颈在边排序,需要 。
- 如果权重范围限制在 ,我们可以使用计数排序 (Counting Sort)。回想一下计数排序的时间复杂度是多少?
- 如果权重范围限制在 且 为常数,同样可以用计数排序。排序后,Kruskal 算法的瓶颈会变成什么?
【深度解析】:
- 当边权限制在 时:
- 排序阶段:我们可以使用计数排序 (Counting Sort) 对 条边进行排序。因为最大值 (由于图是连通的),计数排序的时间复杂度为 。
- 并查集阶段:在边有序后,我们依次取出边进行
connected和union操作。总共会进行 次connected和 次union操作。 - 使用带有路径压缩和按秩合并的并查集,这些操作的总耗时为 。
- 总结运行时间:排序 + 并查集 = 。
- 当边权限制在 ( 为常数)时:
- 排序阶段:同样使用计数排序,时间复杂度为 。因为 是常数,所以排序时间为 。
- 并查集阶段:依然是 。
- 总结运行时间:。
💡 笔记联动:这完美应证了你手写笔记最后一页写到的 “Kruskal’s with pre-sorted edges runs in ”(注: 与反阿克曼函数 在渐进意义上都代表极缓慢增长的准常数)。
第三阶段:经典高阶思考题 (次小生成树)
为了让你的 MST 理论认知达到卓越水平,建议攻克这道 CLRS 经典的课后大题。
✍️ 思考题 21-1:次小生成树 (Second-best Minimum Spanning Tree)
- 背景介绍: 给定一个带权无向连通图 。我们知道它可能有多棵生成树。其中总权重最小的称为最小生成树 (MST)。而总权重第二小的生成树(可以与 MST 权重相等,只要它是不同的边集组合)被称为次小生成树 (Second-best MST)。
- 问题设计:
- 设 是 的唯一一棵 MST。证明:次小生成树 一定可以通过“替换一条边”从 得到。也就是说,存在一条边 和一条边 ,使得 。
- 给出高效算法的系统设计思路:如何在 的时间内找到这棵次小生成树?
- 核心考点: 生成树的性质变换、路径上最大边查询。
- 解题思路提示 (Hint):
- 对于第一问:利用代数结构(或反证法),假设 与 有多于一条边的差异,证明可以用更小的一步替换来构造出介于其间的生成树。
- 对于第二问:如果我们把任意一条不在 中的边 (权重为 )加入 ,会形成环。为了让新生成的树权重增加最少,我们应该删去该环上权重最大的一条边(设为 )。
- 那么替换后的新权重为 。我们需要遍历所有不在 中的边,找到使这个增量最小的替换。
- 如何预先计算出树 中任意两点 路径上的最大边权重 ?可以在树 上运行一次 DFS/BFS 吗?
第一问证明:
假设 是 的唯一 MST, 是次小生成树。 设距离(不同边的数量)最小的次小生成树不满足只相差一条边的条件,即 与 相差 条边()。
- 既然 ,必定存在一条边 且 。
- 将 加入 会产生一个唯一的环 。环 中必定存在一条边 且 。
- 根据 MST 的性质,必有 。
- 我们构造一棵新树 。这棵新树与 的差异减少到了 条边。
- 且 。
- 因为 是唯一的 MST,且 也是一棵生成树,所以:
- 若 ,则 根本不是次小生成树(因为 比它更小且不等于 ),产生矛盾。
- 若 ,则 是一棵与 差异更小( 条边)的次小生成树。
- 通过数学归纳,我们总能将差异收缩到刚好 1 条边。因此,次小生成树必然可以通过从 中移去一条边 并加入一条非树边 得到。
第二问算法设计思路:
基于第一问的结论,次小生成树的值为:
其中 是树 中 到 唯一路径上的最大权重边。
高效算法步骤:
- 第一步:运行 Prim 或 Kruskal 算法求出 MST 。时间开销:。
- 第二步:对于树 中的每一个顶点 ,进行一次 DFS:
- 这次 DFS 只在树 的边上进行,用来寻找从 出发到达其它所有节点 的路径。
- 在 DFS 遍历过程中,维护并记录路径上的最大权重边 。
- 单次 DFS 耗时 (因为树只有 条边)。对所有 个顶点各做一次,总耗时为 。
- 第三步:遍历所有不在 中的边 :
- 计算替换代价:。
- 找出所有边中 最小的值。
- 这一步最多遍历 条边,单次查询 是 的,因此耗时 。
- 第四步:次小生成树的权重即为 。
【总时间复杂度】:
在稠密图中,这比直接暴力穷举所有树的替换要高效得多!