关于文中的标记
【正】 = 我第一次口述这段时讲错了,这里是更正和原因
【补】 = 整理时补的一层
本系列是上下两篇,本篇是下篇,建议先读上篇:
篇 讲什么 一句话 上篇 最小生成树:Kruskal / Prim 把所有点 用最小代价串起来 → 得到一棵树 下篇(本篇) 最短路径:Dijkstra / Bellman-Ford / Floyd 求两个点之间 最近的走法 → 得到一条路 两篇的完整对照放在本篇第二节(上篇末尾留了几个问题,本篇逐个回答)。
一、专题介绍
Q:三个算法都在求最短路,为什么非得三个?
A:因为它们面对的图不一样,而"不一样"这件事,最终只落在三句话上。
| 算法 | 一句话 | 轮数 | 负权 |
|---|---|---|---|
Dijkstra |
挑最小的,定死,不再改 | N |
❌ |
Bellman-Ford |
谁也不定死,反复推翻 | N-1 |
✅ |
Floyd |
每轮多放行一个中转站 | N |
✅ |
Q:为什么 Dijkstra 一旦确认就不改,而 Bellman-Ford 要改 N-1 遍?
A:因为 Dijkstra 赌了一个前提------"绕远路只会更远"。 这个前提在边权非负时成立,负权一进来就作废。Bellman-Ford 不赌,所以它慢,但它对。
重点! 全文三条线:
Dijkstra :猜一个 → 确认一个 → 修一圈 (N 次,确认即终止)
Bellman-Ford :全部试一遍 → 再来一遍 → 共 N-1 遍 (靠重复逼出答案)
Floyd :k 从小到大放行 → 每一层全表扫一遍 (靠分层逼出答案)
这三条线不是三种技巧,是三种"怎么处理不确定性"的态度。
二、最小生成树 vs 最短路径
这一节是两篇的接口。 上篇讲完 MST 后留下五个问题,先回答前两个------因为如果不先把这两个问题分清,后面三个算法会全部和 Kruskal / Prim 混在一起。
2.1 要的东西不一样:一棵树 vs 一条路
| 最小生成树(上篇) | 最短路径(本篇) | |
|---|---|---|
| 输出 | 一棵树 (n 个点、n-1 条边、无环) |
一条路径 (从 s 到 t,边数不固定) |
| 优化对象 | n-1 条边的权值总和 |
单条路径的长度 |
| 关心谁 | 所有点(一个都不能少) | 两个端点(中间的点随便) |
一句话区分:
MST 问"把所有点连起来要多少钱";最短路问"从这里到那里有多远"。
同一个动作在两边含义完全不同 :MST 里"选中一条边"是把它加进树 ;最短路里"选中一条边"是沿着它走过去。
2.2 不连通时,判据相反
| 图不连通时 | |
|---|---|
| Kruskal / Prim | ❌ 返回 false。"生成树"三个字里就含"连通" |
| Dijkstra / Bellman-Ford | ✅ 到不了的点 dist = ∞,那就是答案 |
| Floyd | ✅ dist[i][j] = ∞ 表示"没路",这也是答案 |
连通性是最小生成树的门槛,不是最短路的门槛。
最短路只有一种"失败",叫负环。
【正】坑点 :一开始我在 Dijkstra 末尾写了"只要有一个点 visited == false 就整体 return false,并把数组清空"------那是把 MST 的"全有或全无"搬过来了。
后果是:图里只要有一个孤点,你连其他所有点的正确距离都拿不到 ------明明 A→B 已经算出来了,却因为 C 不可达被一起丢掉。但是"部分成功"在单源最短路里是合法的。
2.3 反直觉:负权处理
| 最小生成树 | 最短路径 | |
|---|---|---|
| 负权边 | ✅ 无害 | ❌ Dijkstra 直接失效 |
| 负环 | ✅ 无关 | ❌ 题目无解 |
为什么 MST 不怕负权? 因为 MST 优化的是"n-1 条边的和",边数是固定的------它不存在"多选一条更划算"这种事,负边照样参与排序和比大小。
为什么最短路怕? 因为最短路的边数不固定:
多走一段可能更短 ← 负权让这句话成立
这就是全部差别:
MST:边数固定 → 负权只是"数字小一点",不影响选择。
最短路:边数可变 → 负权让"绕远"变成"更近",整个贪心前提塌掉。
同一张带负权的图,Kruskal 完全正确,Dijkstra 会算出个错数还不报错。 这是这两篇最值得记的一处反直觉。
2.4 贪心"选"的东西不一样
| 每一步在选什么 | 需要判环吗 | |
|---|---|---|
| Kruskal | 全图最小的边 | 要(并查集) |
| Prim | 跨割最小的边 | 不要(集合划分天然避免) |
| Dijkstra | dist 最小的点,然后确认它 |
不需要(树是副产品,不是目标) |
| Bellman-Ford | 不选,全部边各试一遍 | --- |
| Floyd | 不选,每轮多放行一个中转站 | --- |
注意 Dijkstra 那一格 :它挑的是点不是边------尽管它下一步的动作是"松弛这个点的所有出边"。
【正】我口述时把 Dijkstra 说成"每次选最短的边,以该边终点为起点向外更新"------这句其实是 Prim 的描述。两个算法的分界线,恰恰就是选点还是选边。
2.5 上篇的问题:为什么 MST 不用证明,Dijkstra 必须证明
【补】上篇结尾我留了这个问题,这里回答:
Kruskal / Prim 也做"挑最小的"这个动作,但没人要求你证明它------因为:
MST:选满 n-1 条就结束,边数是固定的
→ "挑最小"只是在给这固定的 n-1 个位置找最便宜的填法
Dijkstra :
最短路:路径长度不固定,你不知道要走几条边
→ "挑最小的点确认"是一次【不可逆的声明】
→ 一旦声明错了,后面再也没有机会改回来
所以:MST 的"挑最小"是"在固定预算里省钱";Dijkstra 的"挑最小"是"下判断"。省钱错了可以换,判断错了不能回头。
三、单源Dijkstra:把猜测变成事实
3.1 一句话思想
每次从未确定的点中选出当前距离最小的点,并用它松弛邻居,逐步确定非负权图的单源最短路径。
分清: dist[] 是猜测,S 是事实
这是理解它的第一步,也是最容易被跳过的一步。
cpp
vector<W> dist; // 目前已知的最好答案 ------ 不一定对
vector<bool> S; // 已经确认是正确答案的点 ------ 进去就不再改
整段代码只做两件事:把猜测变成事实、用事实去改猜测。
| 代码 | 在干什么 |
|---|---|
for (j...) if (!S[j] && dist[j] < min) |
挑一个猜测最小的,准备把它变成事实 |
S[u] = true; |
确认 。从此 dist[u] 是事实 |
for (k...) if (dist[u] + w < dist[k]) |
用刚确认的事实,去修正邻居的猜测 |
外层那个 for (i = 0; i < N; ++i) 只是"重复上面的动作 N 次"------每次只能确认一个点,N 个点就要 N 次。
边界1: N 次,不是 N-1 次。
过程图示:

【正】我口述时说的是"N-1 轮就能确定完所有顶点",这是错的 。N-1 是 Bellman-Ford 的轮数(最多 N-1 段边),而 Dijkstra 是每个点确认一次 ,一共 N 次。两个算法的轮数碰巧都在 N 附近,但来历完全不同。
边界2: 如果把 if (S[k] == false) 这个条件去掉,Dijkstra 就退化成 Bellman-Ford 的一轮。两个算法的距离,就差这一个条件。
3.2 类比选择排序
Dijkstra 更像选择排序 ------挑一个最小的、定死、范围缩小一个。连"负权为什么会毁掉它"都能对上:选择排序的前提是"我挑出来的最小,后面不会冒出更小的",Dijkstra 的前提一模一样。负权一进来,两个前提同时不成立。
3.3 证明 "挑最小的一定对"
这一段是我卡得最久的地方,虽然现在仍然似懂非懂。
证明思路:
Dijkstra 挑 u 的时候,不可能存在一条"绕开已确定区域、从外面更短地到达 u"的路。因为那条路第一次出 S 的点 y,disty 一定比 distu 小,u 就不会被选中。
注意:不是"把所有路径讨论一遍",而是"只看路径第一次离开 S 的那一步"。
假设设 dist[u] 不是答案,存在真实最短路 P 长度 L < dist[u]。沿 P 从源点走,y 是路上第一个不在 S 里的点,x 是 y 在 P 上的前一个点( x ∈ S)。
s ──────── x ──── y ──────── u
(全在 S 里) ↑ ↑
第一个 还在 S 外
出 S 的点
① dist[x] = 真实最短路(s→x) ≤ P 上 s→x 那一截
(真实最短路不会比任何具体路径更长)
② dist[y] ≤ dist[x] + w(x,y)
(x 进 S 时松弛过 y)
③ ⟹ dist[y] ≤ P 上 s→y 那一截
④ P 上 s→y 那一截 ≤ L
(边权非负)
综合:dist[y] ≤ L < dist[u]
但 y 在 S 外、且 dist[y] < dist[u] ------那刚才挑最小的,本来应该挑到y啊,怎么会挑到 u?假设不成立,dist[u] 不可能大于真实最短路。而 dist[u] 也不会小于真实最短路。所以 dist[u] 就是真实最短路。
矛盾。 证毕。
3.4 注意点:负权检查,检查到哪一层
【正】我一开始写的是"检查全图有没有负权边",被自己否掉了------太绝对。
s → a 1 ← 从 s 可达
c ⇄ d −5 ← 从 s 走不到
从 s 出发根本碰不到 c→d,结果完全正确。所以该检查的是"从源点可达的那部分"。
但"可达"本身得先跑一次 BFS 才知道------为了检查再跑一遍,不值。
最后落在两档之间:
cpp
// 只查源点的出边(我现在的写法)
for (int i = 0; i < n; ++i)
if (_edge[start][i] < 0) return false;
代价 :只覆盖了第一步,把s→a 正、a→b 负的情况放过去,并不是最优但是仍有一定效果。
四、单源Bellman-Ford:允许推翻
4.1 一句话思想
通过对所有边反复松弛,不断修正甚至推翻已有的距离估计,直到收敛,从而求出单源最短路径,并能检测负权环
过程图示:

初始 dist = [0, ∞, ∞, ∞, ∞]
第 1 轮
| 扫到 | 试算 | 结果 |
|---|---|---|
s→t |
0+6 | t=6 |
s→y |
0+7 | y=7 |
y→z |
7+9 | z=16 |
y→x |
7−3 | x=4 |
z→s、z→x |
16+2、16+7 | 不动 |
t→x、t→y |
6+5=11、6+8=14 | 不动 |
t→z |
6−4 = 2 | z=2 ← |
x→t |
4−2 = 2 | t=2 ← |
第 1 轮结束:[0, 7, 2, 2, 4]
这里有件事要盯住 :最后两行用的 dist[t]=6,是这一轮刚被 s→t 改出来的。
所以"一轮"不等于"一跳"。 一轮里边的顺序顺路,就能一路接力传下去。
第 2 轮 只有一条能改:t→z,2−4 = −2 < 2 → z = −2。结束 [0, 7, −2, 2, 4]。
第 3 轮 一条都改不动 → exchange == false → 停。
看 dist[z] 这一列:∞ → 16 → 2 → −2。它被改了三次。
而 Dijkstra 里,y 一旦进 S,5 就是 5,永不改变。
Dijkstra 是"一次说定",BF 是"反复推翻"。
4.2 类比冒泡排序
这个类比是这一段里最省记忆的:
| 冒泡排序 | Bellman-Ford | |
|---|---|---|
| 每轮做什么 | 相邻两两比较 | 所有边过一遍,能改小就改 |
| 每轮保证 | 挪一格 | 多传一跳 |
| 轮数 | N-1 |
N-1 |
为什么是 N-1 |
最远的元素要挪 N-1 次 |
最远的信息要传 N-1 跳 |
N-1 都来自"最远的传播距离",不是来自"元素个数"。
但有一处必须改:"更新"不等于"确定"。
| 冒泡 | Bellman-Ford | |
|---|---|---|
| 每轮更新的是什么 | 一个元素的最终位置 | 一个点的当前最好猜测 |
| 会不会被改回去 | 不会(范围排除了) | 会,一直会 |
N-1 轮后 |
必然有序 | 只是"够用了" |
所以代码里的边界完全不同:
cpp
// 冒泡:每轮边界缩小一格
for (int i = 0; i < n-1; ++i)
for (int j = 0; j < n-1-i; ++j) // ← 少了 i 个
// Bellman-Ford:边界从来不变
for (int k = 0; k <= n; ++k)
for (int i = 0; i < n; ++i) // ← 每轮全扫一遍
BF 不敢缩小范围,因为没有任何一个点是定死的。
4.3 N-1 不是"走完所有边",是"最多 N-1 段"
这一条我一开始理解错了,以为循环结束才把全部边走完,但是实际上:
【正】一轮就已经把所有边走完了。 for i × for j 扫的是整个邻接矩阵,E 条边一轮全过完。
所以 N-1 轮是走了 N-1 遍全部边,不是"分批把边走完"。
那 N-1 从哪来?
不重复点的路,最多经过
N-1段;N-1是"运气最差"的轮数,不是"固定次数"。
跟边有多少条一点关系都没有。
最坏VS最好情况 :

4.4 负环无解
什么是负环?判据只有一条:环上所有边的权值加起来 < 0。
| 环 | 权和 | 是负环吗 |
|---|---|---|
A→B(+5)→C(−3)→A(−1) |
+1 |
❌ 里面有两根负边,但不是 |
A→B(−2)→A(−1) |
−3 |
✅ |
A→B(−10)→C(+4)→D(+4)→A |
−2 |
✅ 只有一根负边,但它就是 |
举个例子,s a b三点形成负环回路:
| 路径 | 长度 |
|---|---|
s→a |
0 |
s→a→b→a |
−8 |
s→a→b→a→b→a |
−16 |
只要愿意多绕一圈,就能再来一次 −8。 注意:这不是"算错了",是这道题没有答案。
边界:有向 / 无向不一样。
- 有向图 :环必须能顺着箭头走回来。
A→B有边但B→A没有,那它不是环 - 无向图 :
A---B两个方向都能走。一根负权边就等于一个负环 (2w < 0)
4.5 注意点:exchange 该放在哪
exchange用于判断该轮选边是否可以提前退出(前提是没有边再参与更新了)。
我一开始写成了"只要边存在就 exchange = true":
cpp
if (_edge[i][j] != W_MAX)
{
exchange = true; // ← 这一行在错的地方
if (dist[j] > dist[i] + _edge[i][j]) { ... }
}
后果 :exchange 恒为 true,永远不会 break,round 永远是 n+1 ------ 所有图都报"存在负权回路",一张都没漏。
然后我做了一件更错的事:把 round == n 改成 round == n+1。
cpp
if (round == n + 1) // 意思是"一次都没提前退出"
结果当然更错,因为判断条件本身没错,错的是它依赖的前提。
这个判断成立的前提是:"没提前退出 = 每一轮都有更新" 。而 exchange 恒为 true 时,这个前提不成立。
这是值得记住的一次:"改参数"修不好,说明问题在参数之外。
边界: 这个坑还有一个隐蔽版本------把 if (exchange == false) break; 放在 for j 里面(而不是 for i 外面),会导致"这一行扫完就退出",等于只跑了一遍内层。位置必须在内层循环外面。
五、多源Floyd:按中转站分层
5.1 一句话思想
通过枚举中间点逐步松弛任意两点间的距离,动态规划地求出全源最短路径。
前两个算法都是单源,问的是"从 s 出发,到各点多远"。Floyd 换了个问法:
从
i到j,如果只允许经过前k个点当中转站,最近是多少?
这个问法里没有"源点",所以它每次都需要让所有点对参与计算。
定义 f(i, j, k) = 从 i 到 j,只允许经过 {0, 1, ..., k} 这些点当中转站的最短距离。
两个细节:
i和j本身不算"经过"------它们是端点k = −1就是"谁都不许经过"------此时只能是直达边
所以初始那张表,就是邻接矩阵。
5.2 注意点:初始化必须先把边权搬进 dist
这和其他两个算法不一样。Dijkstra / BF 是边用边访问 ,Floyd 是边用边拼------它靠的是"已有的表",不是"原图的边"。
所以初始化必须把边权搬进 dist:
cpp
for (int i = 0; i < n; ++i)
for (int j = 0; j < n; ++j)
{
if (_edge[i][j] != W_MAX)
{
dist[i][j] = _edge[i][j]; // ← 搬边权
pPath[i][j] = i; // ← 直达时 j 的前驱就是 i
}
if (i == j) dist[i][j] = 0;
}
我一开始写成这样:
cpp
for (auto d : dist) d.assign(n, W_MAX); // ← 拷贝!assign 白做
auto d 是副本,assign 改的是副本,原数组一个字都没动。如果调用方传进来的是空 vector,后面 dist[i][j] 直接越界。
改成 auto& 之后 assign 才真正扩容。但即使改对了 &,这一句本身也该删 ------因为紧接着就要逐格赋边权,前面那次全填 W_MAX 是白干。
设计点: for (auto x : v) 只在"读"的时候安全。要改 v 本身或它元素的内容,一律 auto&。
5.3 为什么 k 必须在最外层
这是我最久想通的一条,而且"想不通"的原因很具体:以前学的每一个维度,都在回答"到哪了"。
| 子数组 / 子序列 DP | Floyd | |
|---|---|---|
dp[i] 里的 i |
位置 | --- |
| 多出来的那一维 | 没有 | k = 限制(只许经过前 k 个点) |
| 填表顺序 | i 从小到大,天然就对 |
k 必须最外层,不天然 |
关键差别在最后一行。 我以前写的 DP,维度的遍历顺序天然就是依赖顺序;Floyd 的 k 是三个维度里"最小"的那个,却必须最外层。
为什么?两个理由:
理由一:表从不清空。
vvDist 始终是同一个数组,每层直接在它上面覆盖。所以第 k 层开始时,里面装的是第 k-1 层的成品,而那个成品里已经包含了 0..k-1 的成果。
| 走到 | 这一层新放行 | 表里累积成了 |
|---|---|---|
k=0 |
点 0 | {0} |
k=1 |
点 1 | {0, 1} |
k=2 |
点 2 | {0, 1, 2} |
每一层只"新加一个",但加在前一层的结果上。
理由二:k 想当中间人,得先当够"别人"。
在 k-1 层里,f(i,k) 和 f(k,j) 这些格子已经被反复更新过很多次 了------它们是 k-1 层里的普通端点,用到了 0..k-1 所有已放行的中转站。
所以轮到 k 层、要用"经过 k"这个选项时,"怎么走到 k""从 k 怎么走回来"都已经是当前最优了。
证明正确性:

5.4 注意点:更新时只判两个格子的连通性
这条是口述时我特意提的,因为它最容易写顺手写错:
cpp
// 错:判了 i、j 是否直接连通
if (_edge[i][j] != W_MAX && dist[i][k] + dist[k][j] < dist[i][j])
// 对:只判 i→k 和 k→j 通不通
if (dist[i][k] != W_MAX && dist[k][j] != W_MAX && dist[i][k] + dist[k][j] < dist[i][j])
为什么?因为你正在做的事就是"通过 k 绕过去"。如果 i→k 或 k→j 都走不通,这条路本来就不成立------至于 i→j 有没有直达边,完全无所谓,绕过 k 之后的路径跟直达边没有关系。
顺便,这里判的是 dist 不是 _edge。因为你要用的是"已经拼出来的结果",不是"原图的一条边"。这也正是 5.2 的初始化必须搬边权的原因。
5.5 注意点:pPath 是继承,不是赋值
cpp
pPath[i][j] = pPath[k][j]; // ← 不是 = k
新的路是 i → k → ... → j。注意中间那个 ... :k 到 j 可能还绕了好几个点。

所以:
j的前一步不是k,而是"k→j这条路里j的前一步"。
而那笔记录,早就存在 pPath[k][j] 里了。
写成 = k,只在 k→j 恰好是直达的时候碰巧正确。一旦 k→j 自己也是绕过来的,前驱就写错,打印出来路径会缺一段。
设计点:pPath 是二维的,pPath[i][j] = 在"i 到 j"这条路上,走到 j 的上一步是谁。 对比 Dijkstra 的 parentPath 是一维(只有一个源点,所以只有一套)。
这也是上篇没出现过的东西------MST 里根本不需要"路径表",因为答案是一棵树,不是一条路。
5.6 判断负环:看对角线
返回前的检查工作:判断是否存在负权回路
cpp
for (int i = 0; i < n; ++i)
if (dist[i][i] < 0) return false;
dist[i][i] 本来是 0。它能变负,只有一条途径:i → ... → i 绕了一圈,而且净赚。那就是负环,没有第二种解释。
这里 Floyd 比 BF 强一档:
| 能查到的负环 | |
|---|---|
| Bellman-Ford | 只有从源点可达的 |
| Floyd | 全图的 |
BF 里从 s 走不到的点,dist 永远是 ∞,根本牵扯不进来;Floyd 每个点都当过起点,所以角落里孤立的负环也看得见。
六、综合对比
6.1 九个维度
| Kruskal | Prim | Dijkstra | Bellman-Ford | Floyd | |
|---|---|---|---|---|---|
| 属于 | 最小生成树(上篇) | 最小生成树(上篇) | 最短路(本篇) | 最短路(本篇) | 最短路(本篇) |
| 输出 | 一棵树 | 一棵树 | s 到各点的距离 |
s 到各点的距离 |
任意两点 |
| 每步挑什么 | 全图最小边 | 跨割最小边 | dist 最小点 |
不选,全试 | 不选,分层 |
| 判环 | 要(并查集) | 不要(割保证) | --- | --- | --- |
| 轮数 | 选满 n-1 条 |
选满 n-1 条 |
N |
N-1 |
N |
| 图不连通 | ❌ 失败 | ❌ 失败 | ✅ ∞ 是答案 |
✅ ∞ 是答案 |
✅ ∞ 是答案 |
| 负权边 | ✅ 无害 | ✅ 无害 | ❌ 失效 | ✅ | ✅ |
| 负环 | ✅ 无关 | ✅ 无关 | ❌ 给错答案 | ✅ 能检测 | ✅ 能检测 |
| 复杂度 | O(E log E) |
O(E log V) |
O(N²) |
O(N³) |
O(N³) |
(无向图:一根负边就是双向的,负环存在)
6.2 类比和适用范围
复盘那天我给自己画了一张表,把三个算法各自"像什么"记下来:
| 算法 | 适用 | 我写的类比 |
|---|---|---|
Dijkstra |
单源、非负权 | 局部贪心 / 选择排序 |
Bellman-Ford |
单源、可正可负 | 暴力 / 冒泡排序 |
Floyd |
多源、可正可负 | 背包 DP |
【正】该表有三处需要修改:
① Dijkstra ⇒ 单源正权 应该是"非负权"。
0 权边 Dijkstra 完全能用。 判据是 w >= 0,不是 w > 0。课件原话也是"要求图中所有边的权重非负"。
② Bellman-Ford ⇒ 可正可负 后面要补半句。
可正可负没错,但有负环时它不是"算出来",是"检测出来然后报告无解"(详见 7.3)。完整写法是:
单源、可正可负;有负环则无解,只能检出。
③ Floyd ⇒ 背包 DP 类比对,但是共性是:它们多出来的那一维,都不是在描述"到哪了",而是在描述"允许什么"。这一点上确实是同一种东西。
七、边界
7.1 最短路不要求连通
【正】这一条我一开始搞错了,把最小生成树的标准套过来了(详见 2.2)。
正确说法:
连通性是最小生成树的门槛,不是最短路的门槛。
最短路只有一种"失败",叫负环。
7.2 ∞ 不能直接参与算术
这个坑点在三个地方同时出现:
| 位置 | 表现 |
|---|---|
| 我写的 Dijkstra | if (_edge[choose][i] > 0) 判连通,INT_MAX 参与加法溢出成负数 → 被当成"更短",写回 dist |
| 标准 BF | if (_matrix[i][j] != MAX_W && dist[i] + _matrix[i][j] < dist[j]) ------ 没判 dist[i] 是不是 MAX_W |
正确写法:两头都判。
cpp
if (_edge[i][j] != W_MAX // 边存在
&& dist[i] != W_MAX // 起点可达 ← 经常漏这一半
&& dist[i] + _edge[i][j] < dist[j])
另外:W_MAX 用什么?
cpp
INT_MAX // 一加就溢出
INT_MAX / 2 // 加两次还在 int 范围内,能过大部分题(更推荐)
7.3 负环只影响经过它的那些点对
"图里有负环"和"最短路经过负环"是两件事:
s → a → b 一条正常的链
c ⇄ d 一个孤立的负环(从 s 根本走不到)
| 问 | 答案 |
|---|---|
s 到 b 的最短路 |
✅ 正常存在 |
s 到 c 的最短路 |
❌ 不存在 |
所以"在负权回路图中找最短路是一种错误"这句话太宽了。 准确说是:
如果最短路必须经过一个负环,那这个最短路根本不存在------不是算法不行,是题目无解。
"无解"和"算错"是两码事。
八、代码与测试
8.1 代码实现
GraphMatrix.h:
cpp
#pragma once
#include<vector>
#include<map>
#include<iostream>
#include<climits>
#include<queue>
using namespace std;
//value(vertex), weight(edge)
template<class V, class W, W W_MAX = INT_MAX, bool Direction = false>
class Matrix
{
private:
vector<V> _vertexs; //顶点
map<V, size_t> _vertexmap; //顶点和数组下标的映射关系
vector<vector<W>> _edge; //邻接矩阵
public:
Matrix() = default; //默认生成
Matrix(const V* vertexs, size_t n)
{
//初始化顶点
_vertexs.reserve(n); //开出n个顶点的大小
for (size_t i = 0; i < n; ++i)
{
_vertexs.push_back(vertexs[i]);
_vertexmap[vertexs[i]] = i;
}
//初始化边(n*n的邻接矩阵,初始值为无穷大)
_edge.resize(n);
for (size_t i = 0; i < n; ++i)
{
_edge[i].resize(n, W_MAX);
}
}
~Matrix() {} //TODO
//获取顶点对应下标
size_t GetVertexIndex(const V& v)
{
auto it = _vertexmap.find(v);
//找到
if (it != _vertexmap.end())
{
return it->second; //返回下标
}
//优化:抛异常
throw invalid_argument("不存在的顶点");
//return -1;
}
//根据下标添加边
bool addEdgeByIndex(size_t srci, size_t dsti, const W& w)
{
//检查下标合法性
if (srci == dsti || srci >= _vertexs.size() || dsti >= _vertexs.size()) return false;
_edge[srci][dsti] = w;
//无向图:添加另外一条边
if (!Direction) _edge[dsti][srci] = w;
return true;
}
//根据顶点添加边
bool addEdgeByValue(const V& src, const V& dst, const W& w)
{
//获取顶点
size_t srci = GetVertexIndex(src);
size_t dsti = GetVertexIndex(dst);
//添加边
return addEdgeByIndex(srci, dsti, w);
}
//打印图
void Print()
{
////打印顶点和下标的映射关系
//for (size_t i = 0; i < _vertexs.size(); ++i)
//{
// cout << i << " -> " << _vertexs[i] << endl;
//}
//cout << endl;
cout << " ";
//顶部顶点
for (size_t i = 0; i < _vertexs.size(); ++i)
{
cout << _vertexs[i] << " ";
}
cout << endl;
//侧边栏顶点
for (size_t i = 0; i < _vertexs.size(); ++i)
{
cout << _vertexs[i] << " ";
//打印边的权值
for (size_t j = 0; j < _vertexs.size(); ++j)
{
if (_edge[i][j] == W_MAX)cout << "* ";
else cout << _edge[i][j] << " ";
}
cout << endl;
}
cout << endl;
}
void _bfs(size_t srci, queue<size_t>& q, vector<bool>& visit)
{
q.push(srci);
visit[srci] = true;
int level = 0;
while (q.size())
{
++level;
cout << "bfs第" << level << "层" << endl;
size_t levelsize = q.size();
for (size_t i = 0; i < levelsize; ++i)
{
size_t topi = q.front();
q.pop();
cout << topi << "->" << _vertexs[topi] << endl;
for (size_t j = 0; j < _vertexs.size(); ++j)
{
if (!visit[j] && _edge[topi][j] != W_MAX)
{
q.push(j);
visit[j] = true;
}
}
}
}
}
//bfs宽度优先遍历
void bfs(size_t srci)
{
queue<size_t> q;
vector<bool> visit(_vertexs.size(), 0);
//当前顶点
_bfs(srci, q, visit);
//检查是否存在外部非连通顶点
for (size_t i = 0; i < _vertexs.size(); ++i)
{
if (!visit[i])
{
cout << "存在非连通顶点:" << endl;
_bfs(i, q, visit);
}
}
}
//求最短路径:允许不连通,但是不允许最短路必须经过一个负环的情况------------这个最短路根本不存在;不是算法做不到,是题目无解。
//单源不带负权最短路径(已知顶点值,传入源点到其他点的距离/父节点下标的数组)
bool Dijkstra(const V& v, vector<W>& dist, vector<int>& pPath)
{
size_t start = GetVertexIndex(v);
int n = _vertexs.size();
//初始化数组
dist.assign(n, W_MAX);
pPath.assign(n, -1);
vector<bool> visited(n, false); //统计是否进入最小路径
//检查负权
for (int i = 0; i < n; ++i)
{
if (_edge[start][i] < 0)
{
cout << "存在负权边,Dijkstra不能保证答案的正确性。" << endl;
return false;
}
}
//更新起点(可以划分入循环->第一次选边)
dist[start] = 0;
pPath[start] = start;
for (int i = 0; i < n; ++i)
{
//1.选顶点
int choose = -1;
for (int j = 0; j < n; ++j)
{
//未加入最短路径且从起点到该点的距离最小
if (!visited[j] && (choose == -1 || dist[choose] > dist[j]))
{
choose = j;
}
}
//循环出口(未选到点/该点不可达)
if (choose == -1 || dist[choose] == W_MAX) break;
//2.加入visited集合
visited[choose] = true;
//3.松弛选边
for (int i = 0; i < n; ++i)
{
//连通,则比较原距离和拼接距离,更新dist和pPath
if (_edge[choose][i] != W_MAX)
{
if (dist[i] > dist[choose] + _edge[choose][i])
{
dist[i] = dist[choose] + _edge[choose][i];
pPath[i] = choose;
}
}
}
}
cout << "Dijkstra最短路径生成成功!" << endl;
return true;
}
//单源带负权最短路径
bool BellmanFord(const V& v, vector<W>& dist, vector<int>& pPath)
{
size_t start = GetVertexIndex(v);
int n = _vertexs.size();
//初始化数组
dist.assign(n, W_MAX);
pPath.assign(n, -1);
dist[start] = 0;
pPath[start] = start;
//填表
int round = 0;
for (round = 0; round <= n; ++round) //第n轮检测负权回路
{
bool exchange = false;
for (int i = 0; i < n; ++i)
{
for (int j = 0; j < n; ++j)
{
if (dist[i] != W_MAX && _edge[i][j] != W_MAX && dist[j] > dist[i] + _edge[i][j]) //连通且更短:更新父路径和距离
{
exchange = true;
pPath[j] = i;
dist[j] = dist[i] + _edge[i][j];
}
}
}
if (exchange == false) break; //优化:提前退出循环(注意放在第一层循环里!)
}
//判断并返回
if (round == n+1)
{
cout << "存在负权回路,BellmanFord最小路径生成失败!" << endl;
dist.assign(n, W_MAX);
pPath.assign(n, -1);
return false;
}
cout << "BellmanFord最短路径生成成功!" << endl;
return true;
}
//多源带负权最短路径(第二个参数为pPath[i][j]中j位置的上一个顶点下标,不是i!)
bool FloydwarShall(vector<vector<W>>& dist, vector<vector<int>>& pPath)
{
//初始化数组
int n = _vertexs.size();
for (auto& d : dist) d.assign(n, W_MAX);
for (auto& p : pPath) p.assign(n, -1);
for (int i = 0; i < n; ++i)
{
for (int j = 0; j < n; ++j)
{
if (_edge[i][j] != W_MAX)
{
dist[i][j] = _edge[i][j];
pPath[i][j] = i;
}
if (i == j) dist[i][j] = 0;
}
}
//填表
for (int k = 0; k < n; ++k)
{
for (int i = 0; i < n; ++i)
{
for (int j = 0; j < n; ++j)
{
if (dist[i][k] != W_MAX && dist[k][j] != W_MAX && dist[i][j] > dist[i][k] + dist[k][j]) //连通(注意比较的是dist不是_edge,而且只需要判断2个!)且为最短路径
{
dist[i][j] = dist[i][k] + dist[k][j];
pPath[i][j] = pPath[k][j]; //i->...->k->...->j,后半段路径相同,只需更新[i][j],注意不一定是k!
}
}
}
}
//返回:通过对角线判断是否生成负环
for (int i = 0; i < n; ++i)
{
if (dist[i][i] < 0)
{
cout << "存在负权回路,FloydwarShall最小路径生成失败!" << endl;
for (auto& d : dist) d.assign(n, W_MAX);
for (auto& p : pPath) p.assign(n, -1);
return false;
}
}
cout << "FloydwarShall最短路径生成成功!" << endl;
return true;
}
};
8.2 测试用例
测试示例:
| 用例 | 考察点 | 预期 |
|---|---|---|
| A | Dijkstra 全连通全正权 | dist = 0 1 3 4 |
| B | Dijkstra 有不可达点 | dist = 0 1 ∞,返回 true |
| C | BF 负权无负环 | 返回 true,dist = 0 1 3 |
| D | BF 有负环 | 返回 false |
| E | Floyd 无负权 | a→d = 3(a→b→c→d) |
| F | Floyd 有负环 | 返回 false |
cpp
#include "GraphMatrix.h"
#include "LinkGraph.h"
#include<iostream>
#include<vector>
#include<string>
#include<cstring>
#include <sstream>
#include <random>
#include <iomanip>
using namespace std;
using Graph = LinkGraph<char, int>;
static void banner(const string& s)
{
cout << "\n\n======== " << s << " ========\n";
}
void testMatrix()
{
int num = 10;
vector<int> v(10);
for (int i = 0; i < 10; ++i) v[i] = num++;
//for (auto x : v) cout << x << ' ' << endl;
Matrix<int, int> gh(v.data(), 10);
gh.addEdgeByValue(10, 11, 1);
gh.addEdgeByValue(10, 12, 2);
gh.addEdgeByValue(10, 13, 3);
gh.addEdgeByValue(10, 14, 4);
gh.addEdgeByValue(10, 15, 5);
gh.addEdgeByValue(10, 16, 6);
gh.addEdgeByValue(11, 17, 6);
gh.addEdgeByValue(13, 18, 6);
gh.addEdgeByValue(15, 19, 6);
gh.Print();
cout << endl;
gh.bfs(0);
cout << endl;
vector<int> dist(10, 0);
vector<int> pPath(10, 0);
if (gh.Dijkstra(10, dist, pPath))
{
cout << "最短路径存在!" << endl;
for (int i = 0; i < num; ++i)
{
cout << dist[i] << ' ';
}
cout << endl;
for (int j = 0; j < num; ++j)
{
cout << pPath[j] << ' ';
}
cout << endl;
}
else cout << "最短路径生成失败!" << endl;
}
void TestLinkGraph()
{
string a[] = { "张三", "李四", "王五", "赵六" };
LinkGraph<string, int> g1(a, 4);
g1.addEdgeByValue("张三", "李四", 100);
g1.addEdgeByValue("王五", "李四", 150);
g1.addEdgeByValue("张三", "王五", 200);
g1.addEdgeByValue("李四", "赵六", 70);
g1.addEdgeByValue("张三", "赵六", 90);
g1.addEdgeByValue("王五", "赵六", 30);
g1.Print();
//g1.dfs("张三");
cout << endl;
LinkGraph<string, int> g1mintree;
int total1 = 0;
if (g1.Kruskal(g1mintree, total1))
{
g1mintree.Print();
cout << "g1可以形成最小生成树,权值为:" << total1 << endl;
}
cout << endl;
//非连通图
string b[] = { "张三", "李四", "王五", "赵六", "田七"};
LinkGraph<string, int> g2(b, 5);
g2.addEdgeByValue("张三", "李四", 100);
g2.addEdgeByValue("王五", "李四", 150);
g2.addEdgeByValue("张三", "王五", 200);
g2.addEdgeByValue("李四", "赵六", 70);
g2.addEdgeByValue("张三", "赵六", 90);
g2.addEdgeByValue("王五", "赵六", 30);
g2.Print();
cout << endl;
LinkGraph<string, int> g2mintree;
int total2 = 0;
if (g2.Kruskal(g2mintree, total2))
{
g2mintree.Print();
cout << "g2可以形成最小生成树,权值为:" << total2 << endl;
}
else cout << "g2无法形成最小生成树" << endl;
}
void testPrim()
{
const char* str = "abcdefghi";
LinkGraph<char, int> g(str, strlen(str));
g.addEdgeByValue('a', 'b', 4);
g.addEdgeByValue('a', 'h', 8);
//g.addEdgeByValue('a', 'h', 9);
g.addEdgeByValue('b', 'c', 8);
g.addEdgeByValue('b', 'h', 11);
g.addEdgeByValue('c', 'i', 2);
g.addEdgeByValue('c', 'f', 4);
g.addEdgeByValue('c', 'd', 7);
g.addEdgeByValue('d', 'f', 14);
g.addEdgeByValue('d', 'e', 9);
g.addEdgeByValue('e', 'f', 10);
g.addEdgeByValue('f', 'g', 2);
g.addEdgeByValue('g', 'h', 1);
g.addEdgeByValue('g', 'i', 6);
g.addEdgeByValue('h', 'i', 7);
g.Print();
cout << endl;
LinkGraph<char, int> pminTree;
char c = 'a';
if (g.Prim(pminTree, c))
{
cout << "成功生成g的最小生成树:" << endl;
pminTree.Print();
}
else
{
cout << "g不存在最小生成树!" << endl;
}
}
static const long long INFLL = (1LL << 40);
// 临时屏蔽算法内部的 cout("生成成功"那些话),避免刷屏
struct Silence {
ostringstream oss;
streambuf* old;
Silence() { old = cout.rdbuf(oss.rdbuf()); }
~Silence() { cout.rdbuf(old); }
};
static string sd(int x) {
if (x == INT_MAX) return "INF";
return to_string(x);
}
// 参考实现:标准 Bellman-Ford,用 long long 防溢出
static vector<long long> refBF(int n, const vector<vector<long long>>& m, int s, bool& negCycle)
{
negCycle = false;
vector<long long> d(n, INFLL);
d[s] = 0;
for (int r = 0; r < n - 1; ++r) {
bool ch = false;
for (int i = 0; i < n; ++i)
for (int j = 0; j < n; ++j)
if (m[i][j] != INFLL && d[i] != INFLL && d[i] + m[i][j] < d[j]) {
d[j] = d[i] + m[i][j];
ch = true;
}
if (!ch) break;
}
for (int i = 0; i < n; ++i)
for (int j = 0; j < n; ++j)
if (m[i][j] != INFLL && d[i] != INFLL && d[i] + m[i][j] < d[j])
negCycle = true;
return d;
}
static bool sameDist(int a, long long b) {
if (a == INT_MAX) return b >= INFLL / 2;
return (long long)a == b;
}
void TA()
{
banner("A Dijkstra 全连通全正权 expect 0 1 3 4");
const char vs[] = { 'A','B','C','D' };
Matrix<char, int, INT_MAX, true> g(vs, 4);
g.addEdgeByValue('A', 'B', 1); g.addEdgeByValue('A', 'C', 4);
g.addEdgeByValue('B', 'C', 2); g.addEdgeByValue('B', 'D', 5);
g.addEdgeByValue('C', 'D', 1);
vector<int> dist, p;
bool ok = g.Dijkstra('A', dist, p);
cout << "返回=" << ok << " dist="; for (int x : dist) cout << sd(x) << ' '; cout << "\n";
}
void TB()
{
banner("B Dijkstra 有不可达点 expect 0 1 INF");
const char vs[] = { 'A','B','C' };
Matrix<char, int, INT_MAX, true> g(vs, 3);
g.addEdgeByValue('A', 'B', 1);
vector<int> dist, p;
bool ok = g.Dijkstra('A', dist, p);
cout << "返回=" << ok << " dist="; for (int x : dist) cout << sd(x) << ' '; cout << "\n";
}
void TC()
{
banner("C BellmanFord 负权无负环 expect true / 0 1 3");
const char vs[] = { 's','a','b' };
Matrix<char, int, INT_MAX, true> g(vs, 3);
g.addEdgeByValue('s', 'a', 1); g.addEdgeByValue('a', 'b', 2);
g.addEdgeByValue('s', 'b', 10);
vector<int> dist, p;
bool ok = g.BellmanFord('s', dist, p);
cout << "返回=" << ok << " dist="; for (int x : dist) cout << sd(x) << ' '; cout << "\n";
}
void TD()
{
banner("D BellmanFord 有负环 expect false");
const char vs[] = { 's','a','b' };
Matrix<char, int, INT_MAX, true> g(vs, 3);
g.addEdgeByValue('s', 'a', 1); g.addEdgeByValue('a', 'b', 2);
g.addEdgeByValue('b', 's', -5);
vector<int> dist, p;
bool ok = g.BellmanFord('s', dist, p);
cout << "返回=" << ok << " dist="; for (int x : dist) cout << sd(x) << ' '; cout << "\n";
}
void TE()
{
banner("E Floyd 无负权 expect true / a->d = 3");
const char vs[] = { 'a','b','c','d' };
int n = 4;
Matrix<char, int, INT_MAX, true> g(vs, n);
g.addEdgeByValue('a', 'b', 1); g.addEdgeByValue('b', 'c', 1);
g.addEdgeByValue('c', 'd', 1); g.addEdgeByValue('a', 'd', 100);
vector<vector<int>> dist(n, vector<int>(n, INT_MAX));
vector<vector<int>> p(n, vector<int>(n, -1));
bool ok = g.FloydwarShall(dist, p);
cout << "返回=" << ok << " a->d=" << sd(dist[0][3]) << " a->b=" << sd(dist[0][1]) << "\n";
}
void TF()
{
banner("F Floyd 有负环 expect false");
const char vs[] = { 's','a','b' };
int n = 3;
Matrix<char, int, INT_MAX, true> g(vs, n);
g.addEdgeByValue('s', 'a', 1); g.addEdgeByValue('a', 'b', 2);
g.addEdgeByValue('b', 's', -5);
vector<vector<int>> dist(n, vector<int>(n, INT_MAX));
vector<vector<int>> p(n, vector<int>(n, -1));
bool ok = g.FloydwarShall(dist, p);
cout << "返回=" << ok << "\n";
}
void Stress()
{
banner("对拍:200 张随机图(4~7 点,约 1/3 含负权)");
mt19937 rng(20261101);
int failBF = 0, failFloyd = 0, failDij = 0;
int total = 200, dijTotal = 0, negGraph = 0;
string firstBF, firstFloyd, firstDij;
for (int iter = 0; iter < total; ++iter)
{
int n = 4 + (int)(rng() % 4);
bool allPos = (iter % 3 != 0);
int lo = allPos ? 1 : -6;
int hi = 12;
int pct = 25 + (int)(rng() % 40);
vector<vector<long long>> mll(n, vector<long long>(n, INFLL));
vector<char> vs(n);
for (int i = 0; i < n; ++i) vs[i] = (char)('A' + i);
Matrix<char, int, INT_MAX, true> g(vs.data(), n);
uniform_int_distribution<int> pick(0, 99);
uniform_int_distribution<int> wgen(lo, hi);
for (int i = 0; i < n; ++i)
for (int j = 0; j < n; ++j)
if (i != j && pick(rng) < pct) {
int w = wgen(rng);
mll[i][j] = w;
g.addEdgeByValue(vs[i], vs[j], w);
}
// ---- Bellman-Ford:逐源点对比 ----
for (int s = 0; s < n; ++s)
{
bool neg = false;
vector<long long> ref = refBF(n, mll, s, neg);
if (neg) { ++negGraph; continue; }
vector<int> d, p;
bool ok;
{ Silence s1; ok = g.BellmanFord(vs[s], d, p); }
bool bad = (!ok);
if (ok) for (int j = 0; j < n; ++j) if (!sameDist(d[j], ref[j])) { bad = true; break; }
if (bad) {
if (failBF == 0) {
ostringstream os;
os << "图#" << iter << " n=" << n << " 源=" << vs[s] << " 你的返回=" << ok << "\n 你的dist: ";
if (ok) for (int j = 0; j < n; ++j) os << sd(d[j]) << ' ';
else os << "(直接返回了 false)";
os << "\n 标准dist: ";
for (int j = 0; j < n; ++j) os << (ref[j] >= INFLL / 2 ? string("INF") : to_string(ref[j])) << ' ';
firstBF = os.str();
}
++failBF;
}
}
// ---- Floyd:整表对比 ----
{
bool anyNeg = false;
for (int s = 0; s < n; ++s) { bool ng = false; refBF(n, mll, s, ng); if (ng) anyNeg = true; }
vector<vector<int>> fd(n, vector<int>(n, INT_MAX));
vector<vector<int>> fp(n, vector<int>(n, -1));
bool ok;
{ Silence s1; ok = g.FloydwarShall(fd, fp); }
bool bad = (ok == anyNeg);
if (!bad && ok) {
for (int i = 0; i < n && !bad; ++i) {
bool ng = false;
vector<long long> ref = refBF(n, mll, i, ng);
for (int j = 0; j < n; ++j) if (!sameDist(fd[i][j], ref[j])) { bad = true; break; }
}
}
if (bad) {
if (failFloyd == 0) {
ostringstream os;
os << "图#" << iter << " n=" << n << " 你的返回=" << ok << " 标准判断有负环=" << anyNeg;
firstFloyd = os.str();
}
++failFloyd;
}
}
// ---- Dijkstra:只在全正权图上比 ----
if (allPos) {
++dijTotal;
for (int s = 0; s < n; ++s)
{
bool neg = false;
vector<long long> ref = refBF(n, mll, s, neg);
vector<int> d, p;
bool ok;
{ Silence s1; ok = g.Dijkstra(vs[s], d, p); }
bool bad = (!ok);
if (ok) for (int j = 0; j < n; ++j) if (!sameDist(d[j], ref[j])) { bad = true; break; }
if (bad) {
if (failDij == 0) {
ostringstream os;
os << "图#" << iter << " n=" << n << " 源=" << vs[s] << " 你的返回=" << ok;
firstDij = os.str();
}
++failDij;
}
}
}
}
cout << "随机图 " << total << " 张 | 单源对比覆盖负环 " << negGraph
<< " 组 | Dijkstra 对比 " << dijTotal << " 张图\n\n";
cout << "BellmanFord 不一致: " << failBF << "\n";
if (!firstBF.empty()) cout << " [首个失败] " << firstBF << "\n\n";
cout << "Floyd 不一致: " << failFloyd << "\n";
if (!firstFloyd.empty()) cout << " [首个失败] " << firstFloyd << "\n\n";
cout << "Dijkstra 不一致: " << failDij << "\n";
if (!firstDij.empty()) cout << " [首个失败] " << firstDij << "\n";
}
int main()
{
//testMatrix();
//TestLinkGraph();
//testPrim();
//测试最短路径(Dijkstra,BellmanFord,FloydwarShall)
TA(); TB(); TC(); TD(); TE(); TF();
Stress();
return 0;
}
8.3 打印结果
======== A Dijkstra 全连通全正权 expect 0 1 3 4 ========
Dijkstra最短路径生成成功!
返回=1 dist=0 1 3 4
======== B Dijkstra 有不可达点 expect 0 1 INF ========
Dijkstra最短路径生成成功!
返回=1 dist=0 1 INF
======== C BellmanFord 负权无负环 expect true / 0 1 3 ========
BellmanFord最短路径生成成功!
返回=1 dist=0 1 3
======== D BellmanFord 有负环 expect false ========
存在负权回路,BellmanFord最小路径生成失败!
返回=0 dist=INF INF INF
======== E Floyd 无负权 expect true / a->d = 3 ========
FloydwarShall最短路径生成成功!
返回=1 a->d=3 a->b=1
======== F Floyd 有负环 expect false ========
存在负权回路,FloydwarShall最小路径生成失败!
返回=0
======== 对拍:200 张随机图(4~7 点,约 1/3 含负权) ========
随机图 200 张 | 单源对比覆盖负环 202 组 | Dijkstra 对比 133 张图
BellmanFord 不一致: 0
Floyd 不一致: 0
Dijkstra 不一致: 0
九、踩坑速查
| # | 坑 | 症状 | 根因 | 怎么发现的 |
|---|---|---|---|---|
| 1 | INT_MAX 参与加法 |
dist 里出现莫名其妙的负数 |
∞ + w 溢出成负数,被当成"更短"写回 |
用例 A 的 dist 和手算不符 |
| 2 | 判连通用 > 0 |
同上 | INT_MAX > 0 恒成立,等于没判 |
同上 |
| 3 | exchange = true 放错位置 |
所有图都报负环 | 它写在"边存在"分支里,恒为 true,永不 break |
打印 round / exchange |
| 4 | 改判断条件 n → n+1 |
更错 | 判断没错,前提错了;改参数修不好 = 问题在参数之外 | 用例 C(无负边却报负环) |
| 5 | for (auto d : dist) d.assign(...) |
Floyd 全表 ∞ |
auto 是副本,assign 改的是副本 |
cout 一下初始化后的 dist |
| 6 | Floyd 初始化没搬边权 | 同样全表 ∞ |
只填了 W_MAX,没把 _edge 搬进来 |
同上 |
| 7 | Floyd 判了 _edge[i][j] |
绕路的结果更新不上 | 只需判 i→k 和 k→j,不需要判 i→j |
用例 E(a→d 停在 100) |
| 8 | pPath[i][j] = k |
打印路径缺一段 | 应该是 pPath[k][j](继承,不是赋值) |
打印路径时中间少点 |
| 9 | Dijkstra 要求全连通 | 有孤点就整套结果被清空 | 把 MST 的"全有或全无"搬过来了 | 用例 B |
| 10 | 参考实现用 INT_MAX |
对拍"全部不一致" | 标准答案自己溢出,两个都错 | 对拍数全是 909 那种"全军覆没" |
口述时说错的(和代码无关,是表达问题):
| # | 我口述的说法 | 实际情况 |
|---|---|---|
| 11 | "Dijkstra 约 N-1 轮确定所有顶点" | N 轮 。N-1 是 Bellman-Ford 的 |
| 12 | "用小根堆不合适,因为要访问堆内部元素" | 堆能用 ,是不能 decrease-key。标准做法是重复入堆 + 懒惰删除 |
| 13 | "Floyd 类似于背包 DP" | 类比方向对 (多一维都表示"限制放开到哪"),但背包那维顺序可换,Floyd 的 k 必须在最外层。 |
| 14 | "Dijkstra 每次选最短的边,以终点为起点向外更新" | 那是 Prim 。Dijkstra 选的是点 |
| 15 | "在负权回路图中找最短路是一种错误" | 太宽。只有经过负环的那些点对无解;其余正常 |
十、总结
三个算法,三种处理不确定性的态度:
Dijkstra 猜一个、确认一个、修一圈 → 靠"确认"收敛
Bellman-Ford 谁都不确认,反复推翻 → 靠"重复"收敛
Floyd 一次只放行一个中转站 → 靠"分层"收敛
共同点只有一个:它们都在用"已经确定的东西"去改"还没确定的东西"。
- Dijkstra 的"已确定"是
S里的点 - BF 的"已确定"是"上一轮的整张表"
- Floyd 的"已确定"是"
0..k-1层累积出来的表"
差别在于:Dijkstra 敢把猜测升级成事实。
复习重点:
- 先分清本篇和上篇(树 vs 路、不连通的判据相反、负权态度相反)
- Dijkstra 的
N次确认 + 负权为何失效("绕远路可能更近") - 负环判据是"环权和 < 0",不是"有没有负边"
- BF 的
N-1来自"最多 N-1 段边",和实际边数无关 exchange放在内层循环外面 ,且只在实际更新时置true- Floyd 的
k必须在最外层 :表不清空 +k得先当端点攒够距离 - Floyd 更新只判
dist[i][k]和dist[k][j] pPath[i][j] = pPath[k][j],不是= k- 不连通不是失败 ,
dist = ∞就是答案