相关笔记
- 前置笔记:21.1 生长最小生成树
- 关联章节:第20章_基本图算法-章节汇总、第19章_用于不相交集合的数据结构-章节汇总
- 关联概念:不相交集合数据结构、路径压缩、按秩合并、优先队列
概览
本节介绍两种经典的最小生成树(Minimum Spanning Tree, MST)算法——Kruskal算法和Prim算法。两者都基于贪心策略,利用21.1 生长最小生成树中建立的通用MST方法(GENERIC-MST),但选择了不同的安全边选取方式。Kruskal算法从边的视角出发,将所有边按权排序后依次考虑,借助不相交集合数据结构判断是否形成环;Prim算法从顶点的视角出发,从一个起点开始扩展,借助优先队列选择最小权边。
要点列表:
- Kruskal算法的运行时间为 ====,主要开销来自排序 ,并查集操作共
- Prim算法使用二叉堆时运行时间为 ==,使用斐波那契堆时为 ==
- 两种算法的正确性都基于割性质(cut property)
- Kruskal适合稀疏图,Prim适合稠密图
知识结构总览
flowchart TD A["21.2 Kruskal与Prim算法"] --> B["通用MST方法 (GENERIC-MST)"] B --> C["贪心策略: 每次选安全边"] C --> D["Kruskal算法"] C --> E["Prim算法"] D --> D1["核心: 按边权排序"] D --> D2["数据结构: 并查集"] D --> D3["复杂度: O(E lg V)"] D --> D4["适用: 稀疏图"] E --> E1["核心: 从顶点扩展"] E --> E2["数据结构: 优先队列"] E --> E3["二叉堆: O(E lg V)"] E --> E4["斐波那契堆: O(E + V lg V)"] E --> E5["适用: 稠密图"] D --> F["正确性: 割性质"] E --> F
核心思想
2.1 Kruskal算法
核心思路
Kruskal算法的基本策略是:将所有边按权值从小到大排序,依次考虑每条边,如果这条边连接的两个顶点当前不在同一个连通分量中(即不形成环),就将其加入MST。算法维护一个森林(forest),初始时每个顶点自成一棵树,随着边的加入,树逐渐合并,最终形成一棵完整的生成树。
这个过程就像在一个群岛中修建桥梁:先修最便宜的桥,只要两座岛之间还没有通路就修,直到所有岛都连通。
MST-KRUSKAL 伪代码
算法执行流程
- 将所有边按权重从小到大排序
- 初始化 A 为空集,每个顶点通过 MAKE-SET 自成一个独立集合
- 按权重从小到大遍历每条边 (u, v)
- 若 u 和 v 不在同一集合(FIND-SET 不同),将边加入 A,调用 UNION 合并集合
- 遍历完所有边后返回 A,A 即为最小生成树
flowchart TD A["开始:输入图 G, 权值函数 w"] --> B["A = 空集<br/>每个顶点 v 调用 MAKE-SET(v)"] B --> C["将所有边按权重非递减排序"] C --> D["取下一条最小权边 (u, v)"] D --> E{"FIND-SET(u)<br/>≠ FIND-SET(v)?"} E -- 是 --> F["A = A ∪ {(u, v)}<br/>UNION(u, v)"] F --> G{"还有未处理的边?"} E -- 否 --> G G -- 是 --> D G -- 否 --> H["返回 A<br/>A 即为最小生成树"]
MST-KRUSKAL(G, w)
1 A ← ∅
2 for each vertex v ∈ G.V
3 MAKE-SET(v)
4 sort the edges of G.E into nondecreasing order by weight w
5 for each edge (u, v) ∈ G.E, taken in nondecreasing order by weight
6 if FIND-SET(u) ≠ FIND-SET(v)
7 A ← A ∪ {(u, v)}
8 UNION(u, v)
9 return A
Kruskal算法
输入: 连通无向图 ,权值函数 输出: 最小生成树的边集
算法步骤:
- 初始化: ,为每个顶点 创建一个单元素集合(使用 MAKE-SET)
- 排序: 将 中的边按权值 非递减排序
- 贪心选择: 依次考虑每条边 :
- 若 和 属于不同集合(FIND-SET 判断),则 不会形成环
- 将 加入 ,合并 和 所在的集合(UNION)
- 返回: 即为最小生成树的边集
正确性证明
Kruskal算法正确性证明
目标: 证明 Kruskal 算法返回的边集 是 的一棵最小生成树。
证明:
【第一步:建立 Kruskal 森林与并查集的对应关系】 我们将 Kruskal 算法与 GENERIC-MST 框架对应起来。GENERIC-MST 维护一个满足如下条件的边集 : 是某棵最小生成树的子集。在每次迭代中,GENERIC-MST 从安全边集合中选取一条边加入 。
第一步:建立对应关系。 在 Kruskal 算法执行过程中的任意时刻,森林 将 划分为若干连通分量。每个连通分量恰好对应一个由并查集维护的集合。设某时刻森林中的连通分量为 ,则并查集中恰好有 个集合,分别对应这些连通分量。
【第二步:证明被选边是横跨割 的轻量边,由割性质得安全边】 第二步:证明每条被选中的边都是安全边。 设算法正在考虑边 ,且 FIND-SET FIND-SET。令 为包含 的连通分量(即并查集中 所在的集合)。考虑割 :
- 是横跨割 的边(因为 ,)
- 是当前考虑的权值最小的边(因为边已按权值排序,且之前所有权值更小的边要么已被加入 ,要么连接同一连通分量内的顶点而被跳过)
- 中不存在横跨割 的边(否则 和 就会在同一连通分量中,与 FIND-SET 判断矛盾)
因此, 是横跨割 的轻量边(light edge)。由割性质(见 21.1 生长最小生成树), 对 是安全边。
【第三步:归纳论证——初始、归纳步、终止】 第三步:归纳论证。
- 初始: ,显然是某棵 MST 的子集(空集是任何集合的子集)
- 归纳步: 假设当前 是某棵 MST 的子集。算法选出的边 是安全边,因此 也是某棵 MST 的子集
- 终止: 当 包含 条边时, 是一棵生成树。由于 始终是某棵 MST 的子集,且 本身就是生成树,因此 就是一棵 MST
Kruskal 算法正确性得证。
运行时间分析
时间复杂度
Kruskal 算法的运行时间由以下部分组成:
- 初始化(第1-3行): 对 个顶点执行 MAKE-SET,耗时
- 排序(第4行): 对 条边按权值排序,耗时 (因为 ,所以 )
- 主循环(第5-8行): 共 次迭代
- 每次 FIND-SET 操作:(使用按秩合并+路径压缩的并查集)
- 每次 UNION 操作:
- 总计:,其中 是反阿克曼函数(见反阿克曼函数),增长极慢,实践中可视为常数
总运行时间: ====
排序是主要瓶颈,并查集操作的总开销在渐近意义上被排序掩盖。
2.2 Prim算法
核心思路
Prim算法的基本策略是:从一个任意起始顶点开始,维护一个”已到达”顶点集合,每次选择连接已到达集合与未到达集合中权值最小的边,将该边和对应顶点加入MST。算法通过
key数组记录每个未到达顶点连接到已到达集合的最小边权,通过优先队列高效选取最小 key 的顶点。这个过程就像从一座城市开始修路:每次都选择从已修好路的城市到未修路的城市中造价最低的路段,逐步扩展路网直到所有城市连通。
MST-PRIM 伪代码
算法执行流程
- 初始化:源点 r 的 key=0,其余顶点 key=无穷大,所有顶点放入优先队列 Q
- while 优先队列 Q 非空
- 从 Q 中取出 key 最小的顶点 u
- 遍历 u 的每个邻接顶点 v:若 v 仍在 Q 中且 w(u,v) < key[v],更新 key[v]=w(u,v) 和前驱 pi[v]=u
- Q 为空时结束,最小生成树由 A = {(v, pi[v])} 给出
flowchart TD A["开始:输入图 G, 权值 w, 源点 r"] --> B["初始化 key[r]=0<br/>其余 key=无穷大<br/>Q = G.V"] B --> C{"优先队列 Q 非空?"} C -- 是 --> D["u = EXTRACT-MIN(Q)<br/>取出 key 最小的顶点"] D --> E["遍历 u 的每个邻接顶点 v"] E --> F{"v 在 Q 中 且<br/>w(u,v) < key[v]?"} F -- 是 --> G["key[v] = w(u, v)<br/>pi[v] = u"] G --> H{"还有未检查的邻居?"} F -- 否 --> H H -- 是 --> E H -- 否 --> C C -- 否 --> I["结束:返回 A = {(v, pi[v])}<br/>即为最小生成树"]
MST-PRIM(G, w, r)
1 for each u ∈ G.V
2 key[u] ← ∞
3 π[u] ← NIL
4 key[r] ← 0
5 Q ← G.V
6 while Q ≠ ∅
7 u ← EXTRACT-MIN(Q)
8 for each v ∈ G.Adj[u]
9 if v ∈ Q and w(u, v) < key[v]
10 π[v] ← u
11 key[v] ← w(u, v)
Prim算法
输入: 连通无向图 ,权值函数 ,起始顶点 输出: 最小生成树,由前驱子图 给出
关键数据结构:
key[u]:顶点 连接到当前生成树的最小边权(初始为 ,起始顶点 的 key 为 )π[u]: 在最小生成树中的父节点Q:包含所有未加入MST的顶点的最小优先队列,以key值为优先级算法步骤:
- 初始化: 所有
key设为 ,所有π设为 NIL,key[r] = 0- 主循环: 当 非空时:
- 从 中取出
key最小的顶点 (EXTRACT-MIN)- 对 的每个邻居 ,若 仍在 中且 ,则更新
π[v] = u,key[v] = w(u,v)(DECREASE-KEY)
正确性证明
Prim算法正确性证明
目标: 证明 Prim 算法返回的边集 是 的一棵最小生成树。
证明:
【第一步:定义割 , 为已从 中取出的顶点集合】 第一步:定义割。 在算法执行的任意时刻,令 为已经从优先队列 中取出(即已加入MST)的顶点集合, 为仍在 中的顶点集合。则 构成图 的一个割。
【第二步:归纳证明 key[v] = 连接 与 的最小边权】 第二步:证明 key 的含义。 我们断言:对每个顶点 , 等于连接 与 中某个顶点的所有边中的最小权值。即:
用归纳法证明此断言:
- 初始: ,(已取出),对其他顶点 ,若 则 ,否则 。断言成立。
- 归纳步: 假设在取出顶点 之前断言成立。取出 后,。对 的每个邻居 ,算法检查 是否成立。若成立,则更新 ,这恰好反映了 连接到 的最小边权。对 的非邻居顶点,key 不变,断言仍然成立。
【第三步: 横跨割 ,key[u] 是最小横跨边权,故为轻量边】 第三步:证明每次选取的边是安全边。 当算法从 中取出
key最小的顶点 时,令 ( 是使 取得最小值的那个 中的顶点)。则:
- 边 横跨割
- 是所有横跨割 的边中的最小权值(因为 是 中 key 最小的顶点,而 key 恰好记录了每个 中顶点连接到 的最小边权)
- 因此 是横跨割 的轻量边
由割性质(见 21.1 生长最小生成树), 是安全边。
【第四步:归纳论证,与 Kruskal 类似】 第四步:归纳论证。 与 Kruskal 类似,初始 是某 MST 的子集。每次加入安全边后, 仍然是某 MST 的子集。最终 包含 条边,形成生成树,因此就是 MST。
Prim 算法正确性得证。
运行时间分析
时间复杂度——二叉堆实现
使用二叉堆作为优先队列时,Prim 算法的运行时间由以下部分组成:
- 初始化(第1-5行): 构建 个元素的优先队列,耗时
- 主循环(第6-11行):
- 第7行 EXTRACT-MIN:共执行 次,每次 ,总计
- 第9-11行的 DECREASE-KEY 操作:对每条边最多执行一次,共 次,每次 ,总计
总运行时间: ====
时间复杂度——斐波那契堆实现
使用斐波那契堆(Fibonacci heap)作为优先队列时:
- EXTRACT-MIN: 次,斐波那契堆的摊还代价为 每次,总计
- DECREASE-KEY: 次,斐波那契堆的摊还代价为 每次,总计
总运行时间: ====
当图较稠密()时,斐波那契堆的优势明显。当图较稀疏()时,两种实现的渐近复杂度相同,均为 。
2.3 Kruskal vs Prim 对比
选型要点
Kruskal算法适合边数较少的稀疏图,因为它的时间主要花在排序上,边越少排序越快。Prim算法适合边数较多的稠密图,尤其是使用斐波那契堆时,其复杂度仅取决于顶点数和边数的线性组合。在实际工程中,如果图的邻接信息已经以邻接矩阵形式给出,Prim的 简单实现往往是最实用的选择。
| 比较维度 | Kruskal 算法 | Prim 算法 |
|---|---|---|
| 核心视角 | 全局——按边权排序,逐条考虑 | 局部——从一个顶点向外扩展 |
| 贪心策略 | 选权最小且不形成环的边 | 选连接已到达集与未到达集的最小权边 |
| 关键数据结构 | 并查集(Union-Find) | 优先队列(Priority Queue) |
| 二叉堆实现 | ||
| 最优实现 | (排序瓶颈) | (斐波那契堆) |
| 邻接矩阵实现 | ||
| 适合场景 | 稀疏图() | 稠密图() |
| 并行化潜力 | 较好(排序可并行) | 较差(顺序扩展) |
| 边权相同时 | 可能产生不同MST(取决于排序稳定性) | 可能产生不同MST(取决于EXTRACT-MIN的选取) |
Kruskal与Prim的时间复杂度对比定理
对于连通无向图 :
- Kruskal算法的运行时间为 ,无论使用何种优先级队列实现,排序步骤始终是瓶颈
- Prim算法使用二叉堆时为 ,使用斐波那契堆时为
- 当 (即边数渐近多于顶点数)时,Prim 的斐波那契堆实现渐近快于二叉堆实现
- 当 (稀疏图)时,Kruskal 和 Prim(无论哪种堆)的渐近复杂度相同
补充理解与拓展
MST算法的历史
最小生成树问题有着丰富的历史,三种经典算法的发现时间远早于现代计算机科学:
Borůvka算法(1926年):Otakar Borůvka 是最早系统研究 MST 问题的人。他在解决摩拉维亚电力网络最优化的实际问题时提出了这一算法。Borůvka 算法的特点是每一步中每个连通分量同时选择到其他分量的最小权边,因此天然支持并行化,在现代分布式计算中仍有应用。
Jarník算法(1930年):Vojtěch Jarník 独立发现了本质上与 Prim 相同的算法,但他的论文长期被忽视。为表彰他的贡献,该算法在中欧文献中常被称为”Jarník算法”或”Prim-Jarník算法”。
Kruskal算法(1956年):Joseph Kruskal 在贝尔实验室(Bell Labs)工作期间发表。他的论文 “On the shortest spanning subtree of a graph and the traveling salesman problem” 发表于 Proceedings of the American Mathematical Society。值得注意的是,Kruskal 当时只有23岁。
Prim算法(1957年):Robert C. Prim 在贝尔实验室独立发现了该算法。他的论文发表于 Bell System Technical Journal。Edsger Dijkstra 也在1959年独立发现了同一算法,因此该算法有时也被称为”Dijkstra-Prim算法”。
最小生成树的实际工程应用
MST 在工程和科学中有广泛的应用场景:
网络设计与基础设施:
- 通信网络设计:在电信和计算机网络中,MST 用于确定连接所有节点的最优拓扑,最小化线缆长度和建设成本
- 电力传输网络:确定电力线路的最优布局,确保所有区域获得电力供应的同时最小化线路总长度
- 管道铺设:天然气、自来水等管道系统的最优铺设方案
- 交通网络规划:道路网络规划中,用最小总长度连接所有城市
计算机科学与工程:
- VLSI电路布线:在超大规模集成电路设计中,MST 用于优化元件间的连线布局,最小化线长和功耗
- 聚类分析(Single-linkage clustering):计算 MST 后删除权值最大的 条边,将图分割为 个连通分量,每个分量即为一个聚类
- 图像分割:基于像素相似度图构建 MST,用于图像的区域分割
- 近似算法:MST 是旅行商问题(TSP)2-近似算法的基础,也是 Steiner 树问题近似算法的组成部分
- 网络协议:生成树协议(STP, Spanning Tree Protocol)用于防止以太网中的环路,虽然不直接使用 MST,但理论基础相同
数据科学与生物信息学:
- 系统发育树构建:在进化生物学中,MST 用于构建物种间的进化关系树
- 手写字体识别:使用 MST 进行特征提取
来源:Fiveable - Applications of MSTs;UT Dallas - Applications of MSTs;AlgoCademy - Prim’s Algorithm Guide
易混淆点与辨析
辨析:Kruskal vs Prim 适用场景选择
❌ 常见误区: “两种算法时间复杂度都是 ,选哪个都一样”
✅ 正确理解: 虽然使用二叉堆时两者的渐近复杂度相同,但实际性能和最优实现差异显著:
稀疏图(): Kruskal 的排序开销为 ,并查集操作接近线性,整体效率高。Prim 使用二叉堆时 DECREASE-KEY 操作次数虽少(),但 EXTRACT-MIN 仍需 ,两者相当。Prim 使用斐波那契堆时也为 ,优势不大。
稠密图(): Kruskal 的排序开销为 ,而 Prim 使用邻接矩阵的简单实现只需 ,优势明显。Prim 使用斐波那契堆时为 ,同样优于 Kruskal。
图以邻接矩阵给出时: Prim 的 实现无需额外数据结构,代码简洁,通常是最佳选择。
辨析:Prim的key数组 vs Dijkstra的dist数组
❌ 常见误区: “Prim算法和Dijkstra算法几乎一样,只是把dist换成了key”
✅ 正确理解: 两种算法的伪代码结构确实高度相似,但语义有本质区别:
维度 Prim 的 key[v]Dijkstra 的 dist[v]含义 连接到当前生成树的最小边权 源点到 的最短路径估计 更新条件 是否累加 否,只看直接边权 是,累加路径上的边权 解决的问题 最小生成树 单源最短路径 正确性依据 割性质 三角不等式 + 贪心选择 关键区别: Prim 的更新只看直接边权 ,而 Dijkstra 的更新需要加上 (从源点到 的路径总长)。这意味着 Dijkstra 要求所有边权非负,而 Prim 对边权的正负没有限制(虽然MST通常定义在无负权环的图中)。
辨析:MST-KRUSKAL中的UNION操作为什么不会出错
❌ 常见误区: “UNION操作可能会把不该合并的分量合并,导致结果不是MST”
✅ 正确理解: UNION 操作的正确性由以下逻辑保证:
FIND-SET 检查确保无环: 当 FIND-SET FIND-SET 时,说明 和 在不同的连通分量中。加入边 不会在这些分量内部形成环。
UNION 只合并被边连接的分量: UNION 将 和 所在的集合合并,这恰好反映了加入边 后连通关系的变化。
并查集正确维护连通性: 由于每次只加入不形成环的边,并查集始终准确反映当前森林的连通分量划分。MAKE-SET、FIND-SET、UNION 三个操作共同保证了这一不变量。
辨析:斐波那契堆在Prim中的优势
❌ 常见误区: “斐波那契堆总是比二叉堆快,应该优先使用”
✅ 正确理解: 斐波那契堆的优势体现在摊还分析(amortized analysis)层面:
DECREASE-KEY 操作: 斐波那契堆的摊还代价为 ,而二叉堆为 。在 Prim 算法中,DECREASE-KEY 可能被调用 次,这是关键差异所在。
EXTRACT-MIN 操作: 斐波那契堆的摊还代价为 ,与二叉堆相同。
实际开销: 斐波那契堆的常数因子较大,实现复杂。对于中小规模图或稀疏图,二叉堆的实际性能可能更好。斐波那契堆的优势在 时才在渐近意义上体现出来。
工程实践: 由于斐波那契堆实现复杂且常数因子大,许多实际系统使用二叉堆或配对堆(pairing heap)作为替代。
习题精选
| 题号 | 题目描述 | 难度 |
|---|---|---|
| 21.2-1 | 证明对图 的每棵最小生成树 ,都存在一种边的排序方式使得 Kruskal 算法返回 | ⭐⭐ |
| 21.2-2 | 给出 Prim 算法在邻接矩阵表示下的 实现 | ⭐⭐ |
| 21.2-3 | 对稀疏图和稠密图,Prim 的斐波那契堆实现是否渐近快于二叉堆实现? | ⭐⭐⭐ |
| 21.2-5 | 若边权为 到 $ | V |
| 21.2-6 | 若边权在 上均匀分布,Kruskal 和 Prim 哪个能更快? | ⭐⭐⭐⭐ |
| 21.2-8 | Borden 教授的分治 MST 算法是否正确? | ⭐⭐⭐ |
21.2-1 解答
目标: 证明对图 的每棵最小生成树 ,存在一种边的排序方式使得 Kruskal 算法返回 。
证明:
我们构造一种特殊的排序方式。将 的边集 按如下规则排序:
- 首先按权值 非递减排列
- 对于权值相同的边,将属于 的边排在不属于 的边之前
即:若 ,则 排在 前面;若 且 ,则 排在 前面。
论证: 在此排序下,Kruskal 算法首先考虑所有权值最小的边。对于权值相同的边,属于 的边被优先考虑。
【构造排序:同权值边中 的边排前面】 我们需要证明:按此排序执行 Kruskal 算法,恰好选出 的所有 条边。
【对 中边按权值归纳: 在同权值边中最先被考虑, 和 在不同连通分量中】 对 中的边按权值从小到大依次考虑。设当前考虑 中的边 ,权值为 。在 Kruskal 算法执行到考虑 时:
- 所有 中权值小于 的边已经被加入(归纳假设)
- 所有非 中权值小于 的边已被考虑但被跳过(因为它们要么连接同一连通分量,要么已被加入但不会阻止 中边的加入)
- 是当前权值下最先被考虑的边(因为 中的边在同权值边中排最前)
- 此时 和 一定在不同的连通分量中(因为 是树,去掉 后 分成两棵子树, 和 分别在两棵子树中,而之前加入的边都是 中权值更小的边,不会连接这两棵子树)
因此 会被加入 。由归纳法, 的所有边都会被加入,且 Kruskal 算法恰好选出 条边后终止,因此返回的就是 。
21.2-2 解答
目标: 给出 Prim 算法在邻接矩阵表示下的 实现。
思路: 当图以邻接矩阵 表示时,不需要优先队列。维护一个数组 ,其中 记录顶点 连接到当前生成树的最小权边的端点和权值。每次从所有未加入MST的顶点中选择 值最小的顶点加入,然后更新其邻居的 值。
伪代码:
PRIM-ADJ(G, w, r) 1 initialize A with every entry = (NIL, ∞) 2 T ← {r} 3 for i = 1 to |V| 4 if Adj[r][i] ≠ 0 5 A[i] ← (r, w(r, i)) 6 while T ≠ V 7 min ← ∞ 8 for each v ∈ V - T 9 if A[v].weight < min 10 min ← A[v].weight 11 k ← v 12 T ← T ∪ {k} 13 π[k] ← A[k].parent 14 for i = 1 to |V| 15 if Adj[k][i] ≠ 0 and i ∉ T and w(k, i) < A[i].weight 16 A[i] ← (k, w(k, i))复杂度分析:
- 外层 while 循环执行 次
- 每次循环中,第8-11行扫描所有未加入的顶点找最小值,耗时
- 第14-16行扫描所有邻居更新 值,耗时
- 总时间:
总运行时间:
21.2-3 解答
目标: 分析稀疏图和稠密图下 Prim 的斐波那契堆实现是否渐近快于二叉堆实现。
分析:
- 二叉堆实现:
- 斐波那契堆实现:
稀疏图():
- 二叉堆:
- 斐波那契堆:
- 结论:两者渐近相同
稠密图():
- 二叉堆:
- 斐波那契堆:
- 结论:斐波那契堆渐近更快
一般条件: 斐波那契堆实现渐近快于二叉堆实现当且仅当 (即 渐近严格大于 )。设 ,其中 :
- 二叉堆:
- 斐波那契堆:
【渐近比较: 时 】 由于 , 严格大于 (因为 ),且 在 时成立。因此斐波那契堆实现渐近更快。
21.2-5 解答
目标: 若边权为 到 的整数,Prim 算法能多快?若为 到 ( 为常数)呢?
情况一:边权为 到 的整数
可以使用van Emde Boas树(vEB树)作为优先队列。vEB树支持 INSERT、DELETE、EXTRACT-MIN、DECREASE-KEY 操作,每种操作时间为 ,其中 是键值域的大小。
- 键值域 ,因此每次操作
- EXTRACT-MIN: 次,总计
- DECREASE-KEY: 次,总计
- 总运行时间:
与斐波那契堆的 相比,当 时(即较稀疏的图),vEB树实现渐近更快。但此改进不是多项式级别的。
情况二:边权为 到 的整数( 为常数)
由于 是常数,只有 种不同的 key 值。可以使用 个双向链表,每个链表对应一种 key 值,存储具有该 key 的所有顶点。
- EXTRACT-MIN:从最小的非空链表中取出一个顶点,均摊
- DECREASE-KEY:将顶点从一个链表移到另一个链表,
- 总运行时间: (因为图连通,)
当边权范围有限时,Prim 算法可以达到线性时间。
21.2-6 解答
目标: 若边权在 上均匀分布,Kruskal 和 Prim 哪个能更快?
分析:
Kruskal 算法: 主要瓶颈是排序。当边权在 上均匀分布时,可以使用桶排序(bucket sort)在期望 时间内完成排序。排序之后,并查集操作的总摊还时间为 。
Kruskal 的期望运行时间:
Prim 算法: 即使边权分布已知,Prim 算法的 DECREASE-KEY 操作仍然需要对优先队列进行修改。均匀分布并不能显著加速优先队列操作(除非使用特殊的优先队列结构,但这在实践中并不常见)。
Prim 的运行时间: 仍为 (二叉堆)或 (斐波那契堆)
结论: 当边权在 上均匀分布时,Kruskal 算法更快,因为桶排序将排序步骤从 降到了期望 ,使得总期望运行时间为 ,渐近优于 Prim 的 (当 时)。
21.2-8 解答
目标: Borden 教授的分治 MST 算法是否正确?
结论:算法不正确。
反例:
考虑图 ,其中 ,,边权为:
假设算法将顶点划分为 ,。
第一步:递归求解子图。
- ,MST 为空(只有一个顶点)
- ,MST 为 ,权值为
第二步:选择横跨割的最小权边。
- 横跨割 的边为 (权值 )和 (权值 )
- 算法选择其中一条,假设选
第三步:合并。
- 最终结果:,总权值
但真正的 MST 是: ,总权值
错误原因: 算法在递归子问题中选择了 这条权值很大的边,而这条边在全局最优解中根本不应该出现。分治策略的问题在于,子问题的最优解合并后不一定是全局最优解——这是贪心算法和分治算法的本质区别。MST 问题具有贪心选择性质,但不具有最优子结构的分治性质。
视频学习指南
| 资源 | 主题 | 链接 | 说明 |
|---|---|---|---|
| MIT 6.006 Lecture 12 | Minimum Spanning Trees | https://www.youtube.com/watch?v=tKwno5AdXlI | Kruskal与Prim的完整讲解 |
| Abdul Bari | Kruskal’s Algorithm MST | https://www.youtube.com/watch?v=71UQH7Pr9kU | 逐步动画演示Kruskal算法 |
| Abdul Bari | Prim’s Algorithm MST | https://www.youtube.com/watch?v=cplfcGZmX7I | 逐步动画演示Prim算法 |
| WilliamFiset | MST (Kruskal + Prim) | https://www.youtube.com/watch?v=eBbK2irCLKE | 两种算法的对比讲解 |
| NeetCode | Minimum Spanning Tree | https://www.youtube.com/watch?v=4ZlRH0eT-qo | 实战视角的MST问题 |
教材原文
CLRS 第4版 21.2节原文(Kruskal算法)
Kruskal’s algorithm finds a minimum spanning tree by scanning the set of edges in monotonically increasing order by weight. In this algorithm, the set is a forest. At each step, the algorithm adds to the forest an edge of least possible weight that does not form a cycle with edges already chosen. The algorithm terminates when the forest has edges, at which point it is a minimum spanning tree.
The algorithm uses the disjoint-set data structure from Chapter 19 to maintain several disjoint sets of elements. Each set contains the vertices in one tree of the current forest. The procedure FIND-SET returns a representative element from the set that contains . Thus, by testing whether FIND-SET equals FIND-SET, the algorithm can determine whether two vertices and belong to the same tree. The procedure UNION merges the two sets containing and .
CLRS 第4版 21.2节原文(Prim算法)
Like Kruskal’s algorithm, Prim’s algorithm is a special case of the generic MST algorithm. Prim’s algorithm operates much like Dijkstra’s algorithm for finding shortest paths in a graph. Prim’s algorithm has the property that the edges in the set always form a single tree. As the algorithm proceeds, the tree is always a subtree of some minimum spanning tree. The algorithm starts from an arbitrary root vertex and grows the tree until it spans all the vertices in . At each step, a light edge connecting a vertex in to a vertex in is added to the tree.
To implement Prim’s algorithm efficiently, we use a min-priority queue that holds all vertices not yet in the tree, keyed by their key values. The attribute names the parent of in the tree.
参见Wiki
概念页尚未创建
以下概念页尚待创建,创建后可在此补充双向链接:
- 最小生成树(Minimum Spanning Tree)
- 割性质(Cut Property)
- 环性质(Cycle Property)
- Borůvka算法