[C++高阶数据结构·图论下] 最短路径:为什么“挑最小的”一定正确?

关于文中的标记

【正】 = 我第一次口述这段时讲错了,这里是更正和原因

【补】 = 整理时补的一层
本系列是上下两篇,本篇是下篇,建议先读上篇:

篇 讲什么 一句话
上篇 最小生成树: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} 这些点当中转站的最短距离。

两个细节:

  1. i 和 j 本身不算"经过"------它们是端点
  2. 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 敢把猜测升级成事实。

复习重点:

  1. 先分清本篇和上篇(树 vs 路、不连通的判据相反、负权态度相反)
  2. Dijkstra 的 N 次确认 + 负权为何失效("绕远路可能更近")
  3. 负环判据是"环权和 < 0",不是"有没有负边"
  4. BF 的 N-1 来自"最多 N-1 段边",和实际边数无关
  5. exchange 放在内层循环外面 ,且只在实际更新时置 true
  6. Floyd 的 k 必须在最外层 :表不清空 + k 得先当端点攒够距离
  7. Floyd 更新只判 dist[i][k] 和 dist[k][j]
  8. pPath[i][j] = pPath[k][j],不是 = k
  9. 不连通不是失败 ,dist = ∞ 就是答案
相关推荐
weixin199701080162 小时前
官方授权通道和灰色爬虫,做数据服务该选哪条路(附 python 源码)
c++·爬虫·python
wuminyu2 小时前
JVM执行引擎学习总结:热点字节码编译成本地机器码
java·linux·c语言·jvm·c++
Tairitsu_H2 小时前
[C++高阶数据结构·图论上] 最小生成树:为什么“选边”和“选点”是两条路?
数据结构·c++·图论·最小生成树
Bczheng13 小时前
VS2022 安装MFC和QT
c++·qt·mfc
dialect_3 小时前
CODEFORCES ROUND 1127 DIV2
开发语言·c++·算法
悠仁さん11 小时前
【C++】AVL 树
c++
wuminyu12 小时前
JVM规范学习总结:解释器与JIT编译器如何处理JVM字节码
java·linux·c语言·jvm·c++
小徐不徐说12 小时前
序列化与反序列化:把 C++ 对象可靠地保存下来
开发语言·数据库·c++·php
ragnwang12 小时前
Notes: Python pybind11 import C++ module
开发语言·c++·python