最小生成树与最短路径
四个算法你都会写,但 408 一个都不让你写——它让你「动手模拟实现步骤」。
教材对 MST 的要求写得很直白:
对这两种算法应主要掌握算法的本质含义和基本思想,并能动手模拟算法的实现步骤。
所以这一节的全部工作量在手上:Prim 的逐步加点图、Kruskal 的逐步选边图、Dijkstra 的逐轮 dist 表。 三张图表画熟,这一节就结束了。
机制
最小生成树的性质
假设
是一个带权连通无向图, 是顶点集 的一个非空子集。若 是一条具有最小权值的边,其中 , ,则必存在一棵包含边 的最小生成树。
基于该性质的最小生成树算法主要有 Prim 算法和 Kruskal 算法,它们都基于贪心算法的策略。
教材还给了一个通用算法,把两者的共性抽出来:
GENERIC_MST(G){
T=NULL;
while T 未形成一棵生成树;
do 找到一条最小代价边(u,v)并且加入 T 后不会产生回路;
T=T∪(u,v);
}「加入后不会产生回路」这句话就是 Kruskal 需要并查集的原因。
Prim 算法
初始时从图中任取一顶点(如顶点 1)加入树
- 初始化:向空树
中添加图 的任意一个顶点 ,使 , 。 - 循环(重复下列操作直至
):从图 中选择满足 且具有最小权值的边 ,加入树 ,置 , 。
时间复杂度
Kruskal 算法
按权值的递增次序选择合适的边来构造最小生成树:初始时为只有
时间复杂度
边界辨析:
最小生成树的权值之和总是唯一的,但树形不一定唯一。 判据:若图中各边的权值互不相等,则最小生成树唯一;若存在相等权值的边,可能有多棵。 另外,MST 的边数恒为
,且 MST 只对连通无向图有定义——非连通图只有最小生成森林。
Dijkstra 算法
教材表 6.2 的逐轮填表法就是标准答题格式,必须照着写。以教材图 6.17 为例,求从顶点
初始化:集合 dist[] 数组各元素的初始值依次设置为 dist[2]=10、dist[3]=∞、dist[4]=∞、dist[5]=5。
| 顶点 | 第 1 轮 | 第 2 轮 | 第 3 轮 | 第 4 轮 |
|---|---|---|---|---|
| 2 | 10 | 8 | 8 | |
| 3 | 14 | 13 | 9 | |
| 4 | 7 | |||
| 5 | 5 | |||
| 集合 |
每轮两个动作:① 选出 dist[] 中的最小值,把该顶点并入 dist[]。
时间复杂度
关联对照:
教材脚注专门问了「Dijkstra 算法与 Prim 算法有何异同之处」,并给了三条答案:
Prim Dijkstra i. 目的 构建最小生成树 构建单源点的最短路径树 ii. 思路 从一个点开始,每次选择权值最小的边,将其连接到已构建的生成树上 每次找出到源点距离最近且未归入集合的点,并以这个点为基础更新源点到其他所有顶点的距离 iii. 适用的图 只能用于带权无向图 可用于带权有向图或带权无向图 第 iii 条最常考。 两者都基于贪心策略,但「选边最小」和「选点距离最小」是两回事—— Prim 比的是这一条边的权,Dijkstra 比的是从源点累计的距离。
边权为负时 Dijkstra 失效
Dijkstra 算法要求边上的权值非负。 若存在负权边,已并入 dist 可能还会被后来的负边改小,而算法不再回头更新它,因此结果可能错误。
Floyd 算法
用于求每对顶点之间的最短路径,采用动态规划思想:递推产生一个
时间复杂度
Floyd 也适用于带权无向图(可视为有向完全图)。
手算模板
Prim:
- 圈定起点,
。 - 每轮:在所有一端在
、另一端在 的边里挑权最小的,加入。 - 画出每一轮后的树形。共
轮。
Kruskal:
- 把所有边按权值从小到大排成一列。
- 逐条看:两端在不同连通分量则选,否则跳过。
- 选够
条就停。
Dijkstra(照表 6.2 的格式):
- 画表:行是顶点,列是轮次,每格写
dist值 + 路径。 - 初始化
dist[]:源点直接可达的填权值,不可达填。 - 每轮:选最小的
dist并标记(该格加粗/画框)→ 并入→ 更新其余各点。 - 最后一行写出每轮的
。 - 共
轮。
Floyd:写出
边界
| 说法 | 判断 | 说明 |
|---|---|---|
| 「最小生成树唯一」 | ❌ | 权值之和唯一,树形不一定唯一;各边权互不相等时才唯一 |
| 「MST 的边数不定」 | ❌ | 恒为 |
| 「MST 对有向图也有定义」 | ❌ | 只对连通无向图 |
| 「Prim 适合稀疏图」 | ❌ | |
| 「Prim 的复杂度与边数有关」 | ❌ | |
| 「Kruskal 每次选权最小的边就一定加入」 | ❌ | 还要判两端是否在不同连通分量 |
| 「Prim 可用于有向图」 | ❌ | 只能用于带权无向图(教材脚注 iii) |
| 「Dijkstra 只能用于有向图」 | ❌ | 有向无向都可以 |
| 「Dijkstra 能处理负权边」 | ❌ | 要求权值非负 |
| 「Dijkstra 每轮确定一个顶点的最短路径」 | ✅ | 被并入 dist 此后不再变 |
| 「Dijkstra 与 Prim 的区别只是目的不同」 | ❌ | 还有思路和适用图两条(教材脚注给了三条) |
| 「Floyd 不能有负权边」 | ❌ | 允许负权边,但不允许负权回路 |
| 「Floyd 只能用于有向图」 | ❌ | 无向图也可以 |
| 「BFS 可求带权图最短路径」 | ❌ | 只对无权图(6.3) |
口径差异:
算竞里这四个算法的写法和 408 差得很远:
算竞 408 / 王道 Prim 堆优化, 朴素 ,且要画出每轮的树形 Kruskal 并查集 + 排序 同上,但要列出每条边的取舍 Dijkstra 优先队列, 朴素 ,且要逐轮填 dist/路径表负权 上 SPFA / Bellman-Ford 只考「Dijkstra 不能有负权、Floyd 不能有负权回路」这个结论 最关键的差别:408 要的是过程表,不是复杂度。 用堆优化 Dijkstra 的思路去做题,会答不出「第 2 轮
dist[3]是多少」这种问题—— 因为堆优化版本根本不维护一张按轮次演进的完整dist表。
对照速查
| 算法 | 解决 | 时间 | 适用图 | 负权 |
|---|---|---|---|---|
| Prim | 最小生成树 | 带权无向图,稠密 | — | |
| Kruskal | 最小生成树 | 带权无向图,稀疏 | — | |
| Dijkstra | 单源最短路径 | 带权有向或无向图 | 不允许负权边 | |
| Floyd | 每对顶点最短路径 | 带权有向或无向图 | 允许负权边,不允许负权回路 |
| Prim vs Dijkstra | Prim | Dijkstra |
|---|---|---|
| 比什么 | 一条边的权 | 从源点累计的距离 |
| 产物 | 最小生成树 | 最短路径树 |
| 适用 | 仅无向 | 有向或无向 |
考点
- Prim 算法构造最小生成树的实例(2015、2017、2018 命题追踪)——画每轮树形。
- Dijkstra 算法求解最短路径的实例(2012、2014、2016、2021 命题追踪)——逐轮填表。
- Prim 与 Dijkstra 的三点区别(教材脚注),尤其适用图不同。
- MST 权值之和唯一、树形不一定唯一。
- Prim
适合稠密图,Kruskal 适合稀疏图。 - Dijkstra 不允许负权边;Floyd 允许负权边但不允许负权回路。
- Floyd 的递推式与
。
链接
- 🏠 返回总览:数据结构第 6 章:图总览
- ⬅️ 上一节:6.3 图的遍历
- ➡️ 下一节:6.4.3~6.4.5 拓扑排序与关键路径
- 🔗 Kruskal 判回路用并查集:5.5.2 并查集
- 🔗 无权图最短路径用 BFS:6.3.1 广度优先搜索
- 🔗 贪心的另一个代表:5.5.1 哈夫曼树
- 📖 名词库:第 6 章名词库