第 32 章结尾白纸黑字写了三件事:
① Floyd 为什么对,以及那个最经典的错误:
k为什么必须在最外层; ② 负权的正经办法:Bellman-Ford 和它的队列优化 SPFA —— 而且你会发现 SPFA 和上一章那份「在负权图上不小心变对了」的堆优化,是同一件事; ③ 负环怎么判:第 n 轮还能松弛成功,就有负环。
第 5 步还①,第 6、7 步还②,第 4 步还③。
上一章把 brute.cpp(Floyd)当了一整章的黑盒 —— 这一章先把盒子打开。
1一句话问题
和第 32 章同一道题,只放开一个条件:边权可以是负数(
-100 ≤ w ≤ 100)。给一张有向图(n 个点、m 条边)和起点
s:
- 如果从 s 出发能走到某个负环,输出一行
NEGATIVE;- 否则输出 n 个数,第 i 个是 s 到 i 的最短距离,走不到的输出
x。⚠ 可能有重边、自环,也可能有从 s 走不到的点。
上一章边权都是正的,-1 不可能是一个真实的距离,拿它当「走不到」的记号很安全。
这一章距离可以是负数 —— -1 是一个完全合法的答案。
★ 答案的记号和数据的取值范围是一对,改了一边就得对一遍另一边。
这类事故不会报错,只会让对拍在某些数据上莫名其妙地红,然后你去查算法 —— 查一整晚。 (第 19 章那条「题目对边界的约定要抄进注释」的另一种形态。)
为什么不是「图里有没有负环」?因为走不到的负环不影响答案: 图的角落里躺着一个负环,可 s 根本过不去,那这道题的答案照样是一串老老实实的距离。
而这句话对三种算法的代价完全不一样:
| 「只算 s 能走到的」这件事 | |
|---|---|
| Floyd | 要手动补一句 d[s][k] < INF —— 它算的是全图,天生不知道 s 是谁 |
| Bellman-Ford / SPFA | 白送 —— dist 从 s 初始化,走不到的点永远是 INF |
★ 同一个题面,一个天生满足、一个必须自己补 ——
这种地方最容易在两份代码之间写出不一致,而且两边各自看都「没毛病」。
第 8 步那个 wrongGlobalNeg.cpp 就是漏了那半句。
2手算一遍:三张图,因为一张装不下
这一章要验四件事,而它们互相排斥,所以正文从头到尾用三张图。 「需要三张」本身就是这一章的一个结论,第 9 步会说清楚为什么。
7 9 5
5 6 4 ┐
6 7 -2 ├ 5 →(4) 6 →(-2) 7 →(3) 4 ← 拐两个弯才最近
7 4 3 ┘
5 4 9 ← 一步直达 4 号,长 9(更差)
6 4 8 ← 从 6 号一步到 4 号,4+8 = 12(也更差)
1 2 -5 ┐
2 3 2 ├ 1 → 2 → 3 → 1,总长 -5+2+1 = -2 ★ 一个负环
3 1 1 ┘
2 5 7 ← 只有这一条把那块连过来,而且是单向的:s 过不去手算:d[5]=0,d[6]=4,d[7]=4-2=2,d[4]=2+3=5(比 9 和 12 都小)。
1、2、3 号从 5 号根本走不到。答案:
x x x 5 0 4 2★ 注意 4 号那个 5:它是拐了两个弯得来的,而两条「一步到位」的近路都更差 —— 这是第 5 步用来照 Floyd 那个 bug 的。
4 4 1
1 2 3
2 3 -2
3 2 -2 ← 2 → 3 → 2 绕一圈是 -4,而 s = 1 走得到 2 号
3 4 1答案:NEGATIVE。绕那个圈可以让距离无限小,「最短路」这三个字失去意义。
5 5 1
4 5 1 ┐
3 4 1 │ 边故意倒着排 —— 第 6 步会看到这一点有多要命
2 3 1 │
1 2 1 ┘ 链:1 →(1) 2 →(1) 3 →(1) 4 →(1) 5
1 5 10 ← 一步直达 5 号,长 10(更差)答案 0 1 2 3 4。★ 到 5 号的最短路必须走满 4 条边 —— 也就是 n−1 条。
这张图是专门用来说明「n−1 轮一轮都不能少」的。
3标准答案:Floyd —— 允许中转的点越来越多
// 标准答案 —— Floyd:把「允许拿哪些点当中转站」当成 DP 的阶段//// 输入:第一行 n m s// 接下来 m 行 `u v w`,表示一条 u → v 的**有向**边,长 w(⚠ **w 可以是负数**,|w| ≤ 100)// ⚠ 可能有重边、自环,也可能有从 s 走不到的点// 输出:如果从 s 出发**能走到**某个负环,输出一行 `NEGATIVE`(最短路不存在);// 否则输出一行 n 个数:第 i 个是 s 到 i 的最短距离,**走不到的输出 `x`**。//// ⚠ 为什么走不到用 `x` 而不是第 32 章那个 `-1`:这一章的距离**可以是负数**,// -1 是一个完全合法的距离。**答案的记号和数据的取值范围是一对,改了一边就得对一遍另一边。**//// ★ 这一章的关键一步,就在下面那三重循环的**顺序**上://// for (k) for (i) for (j) d[i][j] = min(d[i][j], d[i][k] + d[k][j]);// ↑ k 必须在最外层//// 原因不是「大家都这么写」,而是它本来有三维://// f[k][i][j] = 只允许拿 1..k 当中转站时,i 到 j 的最短距离// f[k][i][j] = min( f[k-1][i][j], f[k-1][i][k] + f[k-1][k][j] )// ↑ 不用 k ↑ 用 k(而且只用一次,用两次没意义)//// **k 是阶段,i 和 j 只是表格里的格子。** 阶段必须在最外层 ——// 这和第 21 章那句「依赖谁,就先填谁」是同一句话(这是它第七次登场)。// 而「转移右边用的必须是 k−1 那一层」和第 23 章 01 背包那句// 「转移右边的第一维必须是 i−1」是**同一个形状**,只不过那里滚动掉的是物品、这里是中转站。// ⚠ 第一维能滚掉(写成二维 d[i][j])是因为 f[k][i][k] == f[k-1][i][k]:// 多一个 k 可以中转,对「到 k 的距离」毫无帮助(绕经自己只会更远,除非有负环)。// 把 k 挪到里层就全乱了 —— 那就是 wrongK.cpp,正文第 5 步专门讲。//// ★ 负环怎么判:跑完之后 `d[k][k] < 0` ⇔ k 在某个负环上。// ⚠ 但题目问的是「**从 s 能走到的**负环」,所以还要加一句 `d[s][k] < INF`。// Floyd 看的是全图,它天生不知道 s 是谁 —— 这个过滤是**必须自己补上**的。// (对照 bell.cpp / spfa.cpp:那两份从 s 出发算,这个过滤是白送的。// 正文第 8 步那个 wrongGlobalNeg.cpp 就是漏了这一句。)//// 复杂度 O(n³)。这一章 n 很小,它当标准答案正合适:// 它和 Bellman-Ford「一轮一轮松弛所有边」的想法完全不同,一个是 DP、一个是迭代逼近。
#include <bits/stdc++.h>using namespace std;
const int INF = 0x3f3f3f3f;
int main() { int n, m, s; if (!(cin >> n >> m >> s)) return 0; vector<vector<int>> d(n + 1, vector<int>(n + 1, INF)); for (int i = 1; i <= n; i++) d[i][i] = 0;
for (int i = 0; i < m; i++) { int u, v, w; cin >> u >> v >> w; // ⚠ 自环这一次**不能扔**:负的自环本身就是一个最短的负环 d[u][v] = min(d[u][v], w); // 重边取 min(第 32 章那条) }
// ★ k 在最外层。⚠ 两头都走得到才松弛 —— 有负权时 `INF + (-7) < INF` 是成立的 for (int k = 1; k <= n; k++) for (int i = 1; i <= n; i++) { if (d[i][k] >= INF) continue; for (int j = 1; j <= n; j++) if (d[k][j] < INF) d[i][j] = min(d[i][j], d[i][k] + d[k][j]); }
// 从 s 能走到的负环 for (int k = 1; k <= n; k++) if (d[k][k] < 0 && d[s][k] < INF) { cout << "NEGATIVE\n"; return 0; }
for (int i = 1; i <= n; i++) { if (d[s][i] >= INF) cout << "x"; else cout << d[s][i]; cout << " \n"[i == n]; } return 0;}点「运行 ▶」看结果
Floyd 看起来是三行循环,其实它本来是个三维 DP:
f[k][i][j] = 只允许拿 1..k 当中转站时,i 到 j 的最短距离
f[k][i][j] = min( f[k-1][i][j], f[k-1][i][k] + f[k-1][k][j] )
↑ 不用 k ↑ 用 k(而且只用一次,用两次没意义)k 是阶段,i、j 只是表格里的格子。 阶段必须在最外层 —— 这就是第 21 章那句「依赖谁,就先填谁」,这是它第七次登场。
而「转移右边用的必须是 k−1 那一层」,和第 23 章 01 背包那句
「转移右边的第一维必须是 i−1」是同一个形状:
那里滚动掉的是物品,这里滚动掉的是中转站。
⚠ 第一维能滚掉(写成二维 d[i][j])是因为 f[k][i][k] == f[k-1][i][k]:
多一个 k 可以中转,对「到 k 的距离」毫无帮助 —— 绕经自己只会更远。
① 自环这一次不能扔。 上一章边权都是正的,自环绕一圈只会更远,直接跳过就行。 这一章一条负的自环本身就是一个负环(「我到我自己是 -3」),扔了就漏判。
② 三重循环里那句「两头都走得到才松弛」不是保险,是必须的。
if (d[i][k] >= INF) continue;
… if (d[k][j] < INF) …有负权时 INF + (-7) 比 INF 小 —— 不挡住的话,Floyd 会把「走不到」
误当成一条 10.6 亿长的路,然后顺着它接下去。
(这一句其实上一章就写进去了,当时的注释里说「这个守卫不是为正权数据加的」——
现在兑现了。)
4★ 兑现预告③:负环怎么判
Floyd 跑完之后,判负环只要一行:
for (int k = 1; k <= n; k++)
if (d[k][k] < 0 && d[s][k] < INF) { cout << "NEGATIVE\n"; return 0; }
d[k][k] < 0 的意思是「从 k 出发绕一圈回到 k,居然是负的」——
那不就是 k 在一个负环上吗。后半句 d[s][k] < INF 就是第 1 步说的那个必须手动补的过滤。
// 标准答案 —— Floyd:把「允许拿哪些点当中转站」当成 DP 的阶段//// 输入:第一行 n m s// 接下来 m 行 `u v w`,表示一条 u → v 的**有向**边,长 w(⚠ **w 可以是负数**,|w| ≤ 100)// ⚠ 可能有重边、自环,也可能有从 s 走不到的点// 输出:如果从 s 出发**能走到**某个负环,输出一行 `NEGATIVE`(最短路不存在);// 否则输出一行 n 个数:第 i 个是 s 到 i 的最短距离,**走不到的输出 `x`**。//// ⚠ 为什么走不到用 `x` 而不是第 32 章那个 `-1`:这一章的距离**可以是负数**,// -1 是一个完全合法的距离。**答案的记号和数据的取值范围是一对,改了一边就得对一遍另一边。**//// ★ 这一章的关键一步,就在下面那三重循环的**顺序**上://// for (k) for (i) for (j) d[i][j] = min(d[i][j], d[i][k] + d[k][j]);// ↑ k 必须在最外层//// 原因不是「大家都这么写」,而是它本来有三维://// f[k][i][j] = 只允许拿 1..k 当中转站时,i 到 j 的最短距离// f[k][i][j] = min( f[k-1][i][j], f[k-1][i][k] + f[k-1][k][j] )// ↑ 不用 k ↑ 用 k(而且只用一次,用两次没意义)//// **k 是阶段,i 和 j 只是表格里的格子。** 阶段必须在最外层 ——// 这和第 21 章那句「依赖谁,就先填谁」是同一句话(这是它第七次登场)。// 而「转移右边用的必须是 k−1 那一层」和第 23 章 01 背包那句// 「转移右边的第一维必须是 i−1」是**同一个形状**,只不过那里滚动掉的是物品、这里是中转站。// ⚠ 第一维能滚掉(写成二维 d[i][j])是因为 f[k][i][k] == f[k-1][i][k]:// 多一个 k 可以中转,对「到 k 的距离」毫无帮助(绕经自己只会更远,除非有负环)。// 把 k 挪到里层就全乱了 —— 那就是 wrongK.cpp,正文第 5 步专门讲。//// ★ 负环怎么判:跑完之后 `d[k][k] < 0` ⇔ k 在某个负环上。// ⚠ 但题目问的是「**从 s 能走到的**负环」,所以还要加一句 `d[s][k] < INF`。// Floyd 看的是全图,它天生不知道 s 是谁 —— 这个过滤是**必须自己补上**的。// (对照 bell.cpp / spfa.cpp:那两份从 s 出发算,这个过滤是白送的。// 正文第 8 步那个 wrongGlobalNeg.cpp 就是漏了这一句。)//// 复杂度 O(n³)。这一章 n 很小,它当标准答案正合适:// 它和 Bellman-Ford「一轮一轮松弛所有边」的想法完全不同,一个是 DP、一个是迭代逼近。
#include <bits/stdc++.h>using namespace std;
const int INF = 0x3f3f3f3f;
int main() { int n, m, s; if (!(cin >> n >> m >> s)) return 0; vector<vector<int>> d(n + 1, vector<int>(n + 1, INF)); for (int i = 1; i <= n; i++) d[i][i] = 0;
for (int i = 0; i < m; i++) { int u, v, w; cin >> u >> v >> w; // ⚠ 自环这一次**不能扔**:负的自环本身就是一个最短的负环 d[u][v] = min(d[u][v], w); // 重边取 min(第 32 章那条) }
// ★ k 在最外层。⚠ 两头都走得到才松弛 —— 有负权时 `INF + (-7) < INF` 是成立的 for (int k = 1; k <= n; k++) for (int i = 1; i <= n; i++) { if (d[i][k] >= INF) continue; for (int j = 1; j <= n; j++) if (d[k][j] < INF) d[i][j] = min(d[i][j], d[i][k] + d[k][j]); }
// 从 s 能走到的负环 for (int k = 1; k <= n; k++) if (d[k][k] < 0 && d[s][k] < INF) { cout << "NEGATIVE\n"; return 0; }
for (int i = 1; i <= n; i++) { if (d[s][i] >= INF) cout << "x"; else cout << d[s][i]; cout << " \n"[i == n]; } return 0;}点「运行 ▶」看结果
5★ 动画:k 一层一层加上去,以及把它放错层会怎样
| i\j | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 1 | 0 | -5 | ∞ | ∞ | ∞ | ∞ | ∞ |
| 2 | ∞ | 0 | 2 | ∞ | 7 | ∞ | ∞ |
| 3 | 1 | ∞ | 0 | ∞ | ∞ | ∞ | ∞ |
| 4 | ∞ | ∞ | ∞ | 0 | ∞ | ∞ | ∞ |
| 5 | ∞ | ∞ | ∞ | 9 | 0 | 4 | ∞ |
| 6 | ∞ | ∞ | ∞ | 8 | ∞ | 0 | -2 |
| 7 | ∞ | ∞ | ∞ | 3 | ∞ | ∞ | 0 |
f[k−1][i][j],
f[k−1][i][k] + f[k−1][k][j])
右边那个计数器是这个动画的灵魂:★ 已经算对的格子数。
正确写法下它一层一层往上涨,最后停在 n²(图 A 上是 49 / 49)。
现在把下拉框切成「✗ 把 k 挪到最内层」——
// ✗ 错误版本① —— ★ 本章的主角:Floyd 的 k 没放在最外层//// 和 brute.cpp 只差**三行的顺序**://// brute.cpp: for (k) for (i) for (j) // ✓ 阶段在最外层// 这里: for (i) for (j) for (k) // ✗ 阶段跑到最里层去了//// 这是整个图论里被写错次数最多的一处,而且**它经常给出正确答案** ——// 这才是它可怕的地方(正文第 6 步那张表:300 轮里它只错 XX 轮,其余全对)。//// ★ 为什么错:Floyd 本来是三维的 DP//// f[k][i][j] = 只允许拿 1..k 当中转站时,i 到 j 的最短距离// f[k][i][j] = min( f[k-1][i][j], f[k-1][i][k] + f[k-1][k][j] )//// **k 是阶段**(第 21 章那句「依赖谁就先填谁」),i、j 只是表格里的格子。// 把 k 放到最里层,等于在「还没算完 k−1 那一层」的时候就去用它 ——// `d[i][j]` 拿到的是一堆半成品。//// 具体到这一份:外层固定 i、j 之后,内层把 k 从 1 扫到 n,// 期间用到的 `d[i][k]` 和 `d[k][j]` 里,很多格子**这一轮压根还没被更新过**。// 于是它只找得到「一次中转」附近的路,两次以上的中转经常漏掉。//// ⚠ 它错得非常「安静」:不崩溃、不报错、大部分数据上答案还是对的。// 只有当最短路**必须拐好几个弯**、而且那些弯的编号顺序不巧时才会现形。// ★ 所以这一份是给**生成器**出的一道题:数据里得有「绕好几个中转点才最短」的路。// (正文第 9 步那张表里,让它现形的那一处改动是「掺一条长链进去」。)
#include <bits/stdc++.h>using namespace std;
const int INF = 0x3f3f3f3f;
int main() { int n, m, s; if (!(cin >> n >> m >> s)) return 0; vector<vector<int>> d(n + 1, vector<int>(n + 1, INF)); for (int i = 1; i <= n; i++) d[i][i] = 0; for (int i = 0; i < m; i++) { int u, v, w; cin >> u >> v >> w; d[u][v] = min(d[u][v], w); }
// ✗ 阶段 k 被放到了最里层 for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) for (int k = 1; k <= n; k++) if (d[i][k] < INF && d[k][j] < INF) d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
for (int k = 1; k <= n; k++) if (d[k][k] < 0 && d[s][k] < INF) { cout << "NEGATIVE\n"; return 0; }
for (int i = 1; i <= n; i++) { if (d[s][i] >= INF) cout << "x"; else cout << d[s][i]; cout << " \n"[i == n]; } return 0;}点「运行 ▶」看结果
它在图 A 上跑出 x x x 9 0 4 2:4 号本该是 5(拐两个弯:4−2+3), 它给了 9 —— 那条一步直达的近路。计数器也停在了 28 / 49。
外层固定 i、j 之后,内层把 k 从 1 扫到 n。
可它用到的 d[i][k] 和 d[k][j] 里,很多格子这时候压根还没被更新过 ——
拿半成品去算,结果就是半成品。
⚠ 它最可怕的地方是大部分时候是对的:不崩溃、不报错, 只有当最短路必须拐好几个弯、而且那些弯的编号顺序不巧时才现形。 第 9 步那张表里,300 轮它只错 85 轮 —— 也就是说, 你随手造几组数据试一试,很可能一次都碰不到。
★ 所以它是给生成器出的一道题:数据里得有「绕好几个中转点才最短」的路。
6★ 兑现预告②之一:Bellman-Ford,以及 n−1 这个数字
Floyd 是 O(n³),而且它一口气算了所有点对之间的距离 —— 这道题只要 s 那一行,太浪费了。
// ★ Bellman-Ford —— 负权的正经办法,也是这一章第二个关键一步//// 和 brute.cpp(Floyd)解同一道题,答案必须一模一样。//// ★ 一句话:**把所有边挨个松弛一遍,叫一轮;跑 n−1 轮就够了。**//// 为什么是 n−1,而且一轮不多一轮不少 —— 关键在这个不变量://// 跑完第 i 轮之后,dist[v] ≤「从 s 出发、**最多走 i 条边**能到 v 的最短长度」//// (归纳一行就完了:第 i 轮松弛边 (u,v) 时,dist[u] 已经不差于「最多 i−1 条边」的答案。)//// 而**没有负环时,最短路一定是一条简单路径** —— 它最多经过 n 个点、也就是 n−1 条边。// 所以 n−1 轮之后一定收敛。★ 「n−1」不是背下来的,是「简单路径最多 n−1 条边」这句话。//// ★ 判负环是白送的(第 31 章「判环白送」的第二次)://// 跑完 n−1 轮之后,**再跑一轮**。要是还有边能松弛成功 —— 有负环。//// 为什么:能松弛成功说明存在一条「用了 n 条边还更短」的走法,// n 条边的走法必然重复经过某个点,也就是绕了一个环,而绕它让距离变小 → 那是负环。//// ⚠ 而且这份代码天生只报告「**从 s 能走到的**负环」:// dist 是从 s 初始化的,走不到的点永远是 INF,`if (dist[u] == INF) continue;` 会把它挡住。// ★ 对照 brute.cpp:Floyd 看的是全图,那个过滤必须自己补一句 `d[s][k] < INF`。// **同一个题面,两种算法一个天生满足、一个必须手动补** —— 这种地方最容易写出不一致。//// ⚠ `if (dist[u] == INF) continue;` 这一句在**负权图上是必须的**,不是保险:// `INF + (-7)` 比 INF 小,不挡住的话,走不到的点会被刷出一个「比 INF 小一点」的距离,// 而且顺着边传染。第 32 章那个「以为会传染、实测不会」的坑,在这一章**真的会**发生 ——// 因为那时边权非负。**同一句代码危不危险,取决于数据的取值范围。**//// 复杂度 O(nm):n 轮,每轮扫 m 条边。比 Floyd 的 O(n³) 好在稀疏图上。
#include <bits/stdc++.h>using namespace std;
const int INF = 0x3f3f3f3f;
struct E { int u, v, w; };
int main() { int n, m, s; if (!(cin >> n >> m >> s)) return 0; vector<E> es(m); for (auto& e : es) cin >> e.u >> e.v >> e.w;
vector<int> dist(n + 1, INF); dist[s] = 0;
// ★ n−1 轮就够了:没有负环时,最短路是简单路径,最多 n−1 条边 for (int round = 1; round <= n - 1; round++) { bool changed = false; for (auto [u, v, w] : es) { if (dist[u] == INF) continue; // ⚠ 负权下这一句是必须的,见上面 if (dist[u] + w < dist[v]) { dist[v] = dist[u] + w; changed = true; } } if (!changed) break; // 提前收敛了就不用跑满(不影响正确性) }
// ★ 第 n 轮:还能松弛成功 ⇔ 从 s 能走到一个负环 for (auto [u, v, w] : es) { if (dist[u] == INF) continue; if (dist[u] + w < dist[v]) { cout << "NEGATIVE\n"; return 0; } }
for (int i = 1; i <= n; i++) { if (dist[i] >= INF) cout << "x"; else cout << dist[i]; cout << " \n"[i == n]; } return 0;}点「运行 ▶」看结果
不变量只有一句,归纳一行就证完了:
跑完第 i 轮之后,
dist[v]≤「从 s 出发、最多走 i 条边能到 v 的最短长度」。(第 i 轮松弛边 (u,v) 时,
dist[u]已经不差于「最多 i−1 条边」的答案。)
而没有负环时,最短路一定是一条简单路径 —— 它最多经过 n 个点、也就是 n−1 条边。
★ 所以「n−1」不是背下来的,它就是「简单路径最多 n−1 条边」这句话。
| 点 | 1 | 2 | 3 | 4 | 5 |
| dist | 0 | ∞ | ∞ | ∞ | ∞ |
| ≤0 条边 | 0 | ∞ | ∞ | ∞ | ∞ |
这个动画把不变量的两边并排画出来:第二行是 Bellman-Ford 的 dist,
第三行是另外独立算的「最多走这么多条边」的真值。
两边每一轮都对得上 —— 那句话就不是我说的,是画面上摆着的。
(check:viz 会把三张图的每一帧都验一遍。)
默认停在图 C:链上每一段都是 1,而边是倒着给的,所以一轮只能往前推一格, 到第 4 轮才算完。现在把「轮数」切成「✗ 只跑 n−2 轮」——
// ✗ 错误版本② —— Bellman-Ford 的轮数少了一轮//// 和 bell.cpp 只差一个数字://// bell.cpp: for (int round = 1; round <= n - 1; round++)// 这里: for (int round = 1; round <= n - 2; round++) // ✗ 少跑一轮//// ★ 它专门用来验「n−1 这个数字到底是不是随便写的」。// n−1 的来历是:**没有负环时最短路一定是简单路径,最多经过 n 个点、n−1 条边**;// 而「跑完第 i 轮之后 dist 不差于『最多 i 条边』的答案」。// 少跑一轮,就是「最多 n−2 条边」—— 只要有一条最短路**正好用满 n−1 条边**,它就漏了。//// ⚠ 而这种路在随机图上很少见:点少边多的时候,最短路往往两三条边就走完了。// ★ 所以它也是给**生成器**出的题,而且要的东西和 wrongK.cpp 一样:// 数据里得有**长链** —— 一条必须老老实实一格一格走过去的路。// (正文第 9 步:掺入长链之前它是 0 / 300。)//// ⚠ 另外那句提前收敛的 `if (!changed) break;` 也一起删了。// 留着它的话,图小的时候往往早就收敛完了,这个 bug 会被那句 break 顺手补上 ——// **一个「优化」把 bug 盖住了**,那就验不出「n−1 是不是必须的」。// (这也是一条经验:**做错误版本时,别让无关的优化替它把窟窿补上。**)
#include <bits/stdc++.h>using namespace std;
const int INF = 0x3f3f3f3f;
struct E { int u, v, w; };
int main() { int n, m, s; if (!(cin >> n >> m >> s)) return 0; vector<E> es(m); for (auto& e : es) cin >> e.u >> e.v >> e.w;
vector<int> dist(n + 1, INF); dist[s] = 0;
for (int round = 1; round <= n - 2; round++) { // ✗ 应该是 n − 1 for (auto [u, v, w] : es) { if (dist[u] == INF) continue; if (dist[u] + w < dist[v]) dist[v] = dist[u] + w; } }
for (auto [u, v, w] : es) { if (dist[u] == INF) continue; if (dist[u] + w < dist[v]) { cout << "NEGATIVE\n"; return 0; } }
for (int i = 1; i <= n; i++) { if (dist[i] >= INF) cout << "x"; else cout << dist[i]; cout << " \n"[i == n]; } return 0;}点「运行 ▶」看结果
它输出 NEGATIVE。可图 C 里一条负权边都没有。
道理很简单,但值得停一下:判负环的方法是「再多跑一轮,看还能不能松弛成功」。 少跑一轮 ⇒ 还没收敛 ⇒ 那一轮当然还能松弛成功 ⇒ 它把「没收敛」当成了「有负环」。
★ 一个 bug 同时污染两种输出 —— 这类 bug 最难从现象倒推回原因, 因为你会盯着「为什么误报负环」去查判环那段代码,而毛病根本不在那儿。
for (auto [u, v, w] : es) {
if (dist[u] == INF) continue;
if (dist[u] + w < dist[v]) { cout << "NEGATIVE\n"; return 0; }
}为什么它对,两句话:能松弛成功说明存在一条「用了 n 条边还更短」的走法; n 条边的走法必然重复经过某个点、也就是绕了一个环,而绕它让距离变小 —— 那就是负环。
⚠ 而且它天生只报告「从 s 能走到的」负环(dist 从 s 初始化,走不到的永远是 INF)——
这就是第 1 步那张表里 Floyd 要手动补、它却白送的那件事。
7★ 兑现预告②之二:SPFA —— 而它就是上一章那份代码
Bellman-Ford 每一轮都把 m 条边全扫一遍。可其中绝大多数是白扫的:
一条边 (u, v) 只有在 dist[u] 刚刚变小的时候才可能松弛成功。
先把这句话量出来:
// 把三种算法的「工作量」数出来 —— 光看秒表说不清谁在做什么//// 用法:./count < 输入//// 数三件事:// · Floyd :三重循环跑了多少次(那就是 n³)// · Bellman-Ford :松弛**尝试**了多少次、其中**成功**了多少次// · SPFA :入队多少次、松弛尝试 / 成功多少次//// ★ 第二行那两个数是这一章最值钱的一组对照://// Bellman-Ford 每一轮都把 m 条边全扫一遍,可其中**绝大多数是白扫的** ——// 一条边只有在 dist[u] 刚刚变小时才可能松弛成功。// **SPFA 的全部内容就是「别扫那些白扫的」**:谁变小了就把谁排队。//// 正文第 7 步拿这两个数把「为什么会想到 SPFA」讲成了一个可以量的事实,// 而不是「有个优化叫 SPFA」。//// ⚠ 有负环时 Bellman-Ford / SPFA 会提前报 NEGATIVE 退出,那时数出来的是「到发现为止」的工作量。
#include <bits/stdc++.h>using namespace std;
const int INF = 0x3f3f3f3f;struct E { int u, v, w; };
int main() { int n, m, s; if (scanf("%d %d %d", &n, &m, &s) != 3) return 0; vector<E> es(m); vector<vector<pair<int, int>>> g(n + 1); for (auto& e : es) { if (scanf("%d %d %d", &e.u, &e.v, &e.w) != 3) break; g[e.u].push_back({e.v, e.w}); }
/* ---------- Floyd ---------- */ long long floydSteps = 0; // ⚠ n 一大,Floyd 的 n³ 本身就跑不动了(n = 2000 是 80 亿次)—— // 而「跑不动」正是这一节要说的话,所以大图上直接不跑,只报公式。 bool floydSkipped = (n > 400); if (!floydSkipped) { vector<vector<int>> d(n + 1, vector<int>(n + 1, INF)); for (int i = 1; i <= n; i++) d[i][i] = 0; for (auto& e : es) d[e.u][e.v] = min(d[e.u][e.v], e.w); for (int k = 1; k <= n; k++) for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) { floydSteps++; if (d[i][k] < INF && d[k][j] < INF) d[i][j] = min(d[i][j], d[i][k] + d[k][j]); } }
/* ---------- Bellman-Ford ---------- */ long long bfTry = 0, bfOk = 0; int bfRounds = 0; bool bfNeg = false; { vector<int> dist(n + 1, INF); dist[s] = 0; for (int round = 1; round <= n - 1; round++) { bool changed = false; bfRounds++; for (auto [u, v, w] : es) { if (dist[u] == INF) continue; bfTry++; if (dist[u] + w < dist[v]) { dist[v] = dist[u] + w; bfOk++; changed = true; } } if (!changed) break; } for (auto [u, v, w] : es) { if (dist[u] == INF) continue; if (dist[u] + w < dist[v]) { bfNeg = true; break; } } }
/* ---------- SPFA ---------- */ long long spPush = 0, spTry = 0, spOk = 0; bool spNeg = false; { vector<int> dist(n + 1, INF), cnt(n + 1, 0), inq(n + 1, 0); queue<int> q; dist[s] = 0; q.push(s); inq[s] = 1; spPush++; while (!q.empty() && !spNeg) { int u = q.front(); q.pop(); inq[u] = 0; for (auto [v, w] : g[u]) { spTry++; if (dist[u] + w < dist[v]) { dist[v] = dist[u] + w; spOk++; cnt[v] = cnt[u] + 1; if (cnt[v] >= n) { spNeg = true; break; } if (!inq[v]) { q.push(v); inq[v] = 1; spPush++; } } } } }
printf("n = %d 个点,m = %d 条边,起点 s = %d\n\n", n, m, s); long long n3 = (long long)n * n * n; if (floydSkipped) printf("Floyd :n = %d 时三重循环要跑 %lld 次 —— 这一档直接没跑它\n", n, n3); else printf("Floyd :三重循环 %lld 次(n^3 = %lld)\n", floydSteps, n3); printf("Bellman-Ford :跑了 %d 轮,松弛尝试 %lld 次,其中成功 %lld 次\n", bfRounds, bfTry, bfOk); printf("SPFA :入队 %lld 次,松弛尝试 %lld 次,其中成功 %lld 次\n", spPush, spTry, spOk); printf("\n"); if (bfTry > 0) printf("★ Bellman-Ford 的松弛里只有 %.1f%% 是有用的(%lld / %lld)——\n" " 剩下那些全是白扫的边。SPFA 的全部内容就是「别扫它们」:\n" " 它只尝试了 %lld 次,是 Bellman-Ford 的 %.2f 倍。\n", 100.0 * bfOk / bfTry, bfOk, bfTry, spTry, bfTry ? (double)spTry / bfTry : 0.0); printf("\n判负环:Bellman-Ford 说 %s,SPFA 说 %s\n", bfNeg ? "有" : "没有", spNeg ? "有" : "没有"); return 0;}点「运行 ▶」看结果
本机实测(./genBig 2000,2000 个点、7999 条边,只能在终端里跑):
| 工作量 | |
|---|---|
| Floyd | 三重循环 80 亿次(n³)—— 这一档根本没法跑 |
| Bellman-Ford | 松弛尝试 63 195 次,其中成功只有 5 796 次(9.2%) |
| SPFA | 入队 3 043 次,松弛尝试 12 166 次 |
★ 九成的松弛是白做的。SPFA 的全部内容就是「别做那九成」:谁的 dist 变小了,就把谁排队。
// SPFA —— Bellman-Ford 的队列优化//// 和 brute.cpp / bell.cpp 解同一道题,答案必须一模一样。//// ★ 想法只有一句:**Bellman-Ford 每轮都把所有边扫一遍,可其中绝大多数是白扫的。**//// 一条边 (u, v) 只有在 dist[u] **刚刚变小**的时候才可能松弛成功。// 所以:谁的 dist 变小了,就把谁排队;从队列里取一个点出来,只松弛它的出边。// —— 这就是 SPFA,一个字都没多。//// ★★ 而这里有一件第 32 章埋好的伏笔,现在可以挑明了://// **SPFA 和第 32 章那份「在负权图上不小心答对了」的堆优化 Dijkstra,是同一件事。**//// 把这份代码里的 `queue` 换成「按 dist 排序的小根堆」,它逐字逐句就是那份 fast.cpp// (连那句 `if (d > dist[u]) continue;` 都对应这里的 inq 判重)。// 第 32 章说「堆优化在负权图上 300 轮一次都没错」——// 现在知道原因了:**它当时执行的根本不是 Dijkstra,而是这个算法。**// ⚠ 所以那句话的正确说法始终是:**不成立的是「取出来就定死」那个贪心。**//// ★ 判负环:给每个点记一个 cnt[v] = 「当前这条到 v 的最短路用了几条边」。// 松弛成功时 cnt[v] = cnt[u] + 1;一旦 cnt[v] ≥ n,就说明这条路用了 n 条边 ——// 必然重复经过某个点、绕了个环,而绕它还更短 → 负环。// (和 bell.cpp 那个「第 n 轮还能松弛」是同一个道理的两种记法。)// ⚠ 和 bell.cpp 一样,它天生只报告「从 s 能走到的」负环。//// ⚠ `inq[]` 的含义是「**它现在是不是正在队列里**」,不是「它来过没有」:// 入队时置 1,**出队时必须清 0**。清晚了 / 忘了清,这个点就再也进不来了 ——// 那就是 wrongSpfaInq.cpp。第 30 章「vis 要在入队时打」讲的是另一件事,别记混。//// 复杂度:**最坏还是 O(nm)**(它没有改变 Bellman-Ford 的上界,只是通常快很多)。// ⚠ 「SPFA 已死」这句话说的就是这件事:出题人可以专门造网格状的数据把它卡回最坏。// 正文第 7 步用 count.cpp 把「白扫的边」这个数量出来了。
#include <bits/stdc++.h>using namespace std;
const int INF = 0x3f3f3f3f;
int main() { int n, m, s; if (!(cin >> n >> m >> s)) return 0; vector<vector<pair<int, int>>> g(n + 1); for (int i = 0; i < m; i++) { int u, v, w; cin >> u >> v >> w; g[u].push_back({v, w}); }
vector<int> dist(n + 1, INF), cnt(n + 1, 0), inq(n + 1, 0); queue<int> q; dist[s] = 0; q.push(s); inq[s] = 1;
while (!q.empty()) { int u = q.front(); q.pop(); inq[u] = 0; // ⚠ 出队就得清,它说的是「在不在队里」 for (auto [v, w] : g[u]) { if (dist[u] + w < dist[v]) { dist[v] = dist[u] + w; cnt[v] = cnt[u] + 1; // ★ 这条路用了几条边 if (cnt[v] >= n) { cout << "NEGATIVE\n"; return 0; } if (!inq[v]) { q.push(v); inq[v] = 1; } } } }
for (int i = 1; i <= n; i++) { if (dist[i] >= INF) cout << "x"; else cout << dist[i]; cout << " \n"[i == n]; } return 0;}点「运行 ▶」看结果
第 32 章的 fast.cpp 在负权图上 300 轮一次都没错,当时给的解释是
「它已经不是 Dijkstra 了」。现在可以把话说完:
// 第 32 章 fast.cpp(堆优化 Dijkstra)
auto [d, u] = q.top(); q.pop();
if (d > dist[u]) continue; // 过期的就扔掉
for (auto [v, w] : g[u])
if (d + w < dist[v]) { dist[v] = d + w; q.push({dist[v], v}); }
// 这一章 spfa.cpp
int u = q.front(); q.pop(); inq[u] = 0; // 出队就清标记
for (auto [v, w] : g[u])
if (dist[u] + w < dist[v]) { dist[v] = dist[u] + w; if (!inq[v]) q.push(v); }同一个算法,只差用什么容器。 把 SPFA 的队列换成小根堆,它就长成上一章那副样子;
而那句 if (d > dist[u]) continue; 顶替的正是这里的 inq 判重。
★ 所以第 32 章那句结论要这样收尾: 上一章的堆优化在负权图上给出正确答案,不是因为 Dijkstra 对负权成立, 而是因为它执行的是这一章的 SPFA。
| 那个标记在记什么 | 什么时候清 | |
|---|---|---|
第 30 章 BFS 的 vis |
来过没有 | 永不清(一个点只需进一次) |
这一章 SPFA 的 inq |
现在在不在队里 | 出队时就清 |
两句话不打架,因为它们是两个不同的变量。 BFS 里一个点确实只需进一次(边权都一样长,第一次碰到就是最优); SPFA 里一个点可能进很多次 —— 每次有人把它刷得更小。
★ 「这个标记到底在记什么」比「它叫 vis 还是 inq」重要一百倍。
// ✗ 错误版本⑤ —— SPFA 的 inq 标记,出队时忘了清//// 和 spfa.cpp 只差一行://// spfa.cpp: int u = q.front(); q.pop(); inq[u] = 0; // 出队就清// 这里: int u = q.front(); q.pop(); // ✗ 没清//// ★ 于是 inq[] 的含义被悄悄改成了「**它来过没有**」——// 一个点出队之后 inq 还挂着 1,从此再也进不了队。// 可 SPFA 的整个道理就建立在「一个点的 dist 变小了就得**重新**入队、把新值传下去」上面。// 进不去 = 传不下去 = 一大片点停在半路上的值。//// ⚠ 这里有个很容易记混的地方,正文里特意点破了://// 第 30 章:BFS 的 vis 要在**入队时**打,而且**永不清除** —— 那是为了「别塞两次」;// 这一章: SPFA 的 inq 要在**出队时**清 —— 它说的是「**现在在不在队里**」。//// 两句话不打架,因为**它们是两个不同的变量**。// BFS 里一个点确实只需要进一次(边权都一样长,第一次碰到就是最优);// SPFA 里一个点可能进很多次(每次有人把它刷得更小)。// ★ **「这个标记到底在记什么」比「它叫 vis 还是 inq」重要一百倍。**//// ⚠ 它还有个讨厌的性质:错得**不彻底**。很多图上第一次传播就已经是最优了,// 它照样给出正确答案。要抓它就得让「某个点被刷小两次以上」真的发生 ——// 而这在**有负权边**的图上才常见。
#include <bits/stdc++.h>using namespace std;
const int INF = 0x3f3f3f3f;
int main() { int n, m, s; if (!(cin >> n >> m >> s)) return 0; vector<vector<pair<int, int>>> g(n + 1); for (int i = 0; i < m; i++) { int u, v, w; cin >> u >> v >> w; g[u].push_back({v, w}); }
vector<int> dist(n + 1, INF), cnt(n + 1, 0), inq(n + 1, 0); queue<int> q; dist[s] = 0; q.push(s); inq[s] = 1;
while (!q.empty()) { int u = q.front(); q.pop(); // ✗ 这里少了 inq[u] = 0; for (auto [v, w] : g[u]) { if (dist[u] + w < dist[v]) { dist[v] = dist[u] + w; cnt[v] = cnt[u] + 1; if (cnt[v] >= n) { cout << "NEGATIVE\n"; return 0; } if (!inq[v]) { q.push(v); inq[v] = 1; } } } }
for (int i = 1; i <= n; i++) { if (dist[i] >= INF) cout << "x"; else cout << dist[i]; cout << " \n"[i == n]; } return 0;}点「运行 ▶」看结果
图 B 的正确答案是 NEGATIVE,它却输出 0 -1 1 2 —— ★ 漏报了负环。
因为点进不了第二次,那个「绕一圈更短」的过程走不下去,cnt 也就永远到不了 n。
8另外两种把它写错的方式
// ✗ 错误版本③ —— 不判「走不到」就去松弛。★ 这一次它真的会传染//// 和 bell.cpp 只差一句://// bell.cpp: if (dist[u] == INF) continue; // 走不到的点不参与松弛// 这里: (删掉) // ✗//// ★★ 这一份是第 32 章那笔账的**正面回应**,值得单独停一下。//// 第 32 章我写过一个几乎一模一样的错误版本,然后被实测打脸:// 那一章边权非负,`INF + w` 比 INF **大**,松弛条件 `dist[u] + w < dist[v]` 根本不成立,// 所以「不判可达」在那里**一点事都没有**。//// 这一章边权可以是负的。`INF + (-7)` 比 INF **小** —— 那一句当场成立://// 走不到的点,会被刷出一个「比 INF 小一点」的距离,然后顺着边一路传染出去。//// ⚠ **同一句代码危不危险,取决于数据的取值范围。**// 「这句判断是不是多余的」这种问题,没有脱离数据的答案。//// ⚠ 它还会顺带把负环判错:那些被污染的点让第 n 轮凭空多出可松弛的边,// 于是它有时会对一张根本没有负环的图喊 NEGATIVE。**一个 bug 同时污染两种输出。**//// ★ 所以这一份是给**生成器**出的题:数据里必须有**从 s 走不到的点**// (第 30、32 章那条第三次登场),而且那些点之间还得有边,污染才传得开。
#include <bits/stdc++.h>using namespace std;
const int INF = 0x3f3f3f3f;
struct E { int u, v, w; };
int main() { int n, m, s; if (!(cin >> n >> m >> s)) return 0; vector<E> es(m); for (auto& e : es) cin >> e.u >> e.v >> e.w;
vector<int> dist(n + 1, INF); dist[s] = 0;
for (int round = 1; round <= n - 1; round++) { for (auto [u, v, w] : es) if (dist[u] + w < dist[v]) dist[v] = dist[u] + w; // ✗ 没挡住走不到的点 }
for (auto [u, v, w] : es) if (dist[u] + w < dist[v]) { cout << "NEGATIVE\n"; return 0; }
for (int i = 1; i <= n; i++) { if (dist[i] >= INF) cout << "x"; else cout << dist[i]; cout << " \n"[i == n]; } return 0;}点「运行 ▶」看结果
它输出 NEGATIVE,而图 A 的正确答案是一串距离。
第 32 章我写过一个几乎一模一样的错误版本,然后被实测打脸:
那一章边权非负,INF + w 比 INF 大,松弛条件 dist[u] + w < dist[v] 根本不成立 ——
所以「不判可达」在那里一点事都没有,那份代码和正解一模一样。
这一章边权可以是负的。INF + (-5) 比 INF 小,那一句当场成立:
走不到的点会被刷出一个「比 INF 小一点」的距离,然后顺着边一路传染出去。
而图 A 里走不到的那三个点之间还有一个负环 —— 于是它一路刷下去, 最后连负环都误报了。一个 bug 同时污染两种输出(第二次)。
★ 同一句代码危不危险,取决于数据的取值范围。 「这句判断是不是多余的」这种问题,没有脱离数据的答案。
// ✗ 错误版本④ —— 判负环时不管「s 到底走不走得到它」//// 和 brute.cpp 只差半句://// brute.cpp: if (d[k][k] < 0 && d[s][k] < INF) → NEGATIVE// 这里: if (d[k][k] < 0) → NEGATIVE // ✗ 少了后半句//// ★ 它错的不是算法,是**题面**:题目问的是「从 s 出发能不能走到负环」,// 而 `d[k][k] < 0` 回答的是「图里有没有负环」。图的角落里躺着一个负环,// 可 s 根本走不过去 —— 那道题的答案照样是一串距离,不是 NEGATIVE。//// ★★ 这一份最值钱的地方在于对照://// | | 「只算 s 能走到的」这件事 |// |---|---|// | Floyd(brute.cpp) | 要**手动补**一句 `d[s][k] < INF` —— 它算的是全图 |// | Bellman-Ford / SPFA | **白送** —— dist 从 s 初始化,走不到的点永远是 INF |//// 同一个题面,一个天生满足、一个必须自己补 ——// **这种地方最容易在两份代码之间写出不一致,而且两边各自看都「没毛病」。**// (第 19 章那条「题目对边界的约定要抄进注释」,这是图论版。)//// ⚠ 这一份是给**生成器**出的最刁的一道题:数据里得有「**s 走不到的负环**」。// 而这要求两件事同时成立:图不连通(第 30、32 章那条),并且**孤立的那一块里有负环**。// 正文第 9 步那张表里,它是最后一个从 0 变成非 0 的。
#include <bits/stdc++.h>using namespace std;
const int INF = 0x3f3f3f3f;
int main() { int n, m, s; if (!(cin >> n >> m >> s)) return 0; vector<vector<int>> d(n + 1, vector<int>(n + 1, INF)); for (int i = 1; i <= n; i++) d[i][i] = 0; for (int i = 0; i < m; i++) { int u, v, w; cin >> u >> v >> w; d[u][v] = min(d[u][v], w); }
for (int k = 1; k <= n; k++) for (int i = 1; i <= n; i++) { if (d[i][k] >= INF) continue; for (int j = 1; j <= n; j++) if (d[k][j] < INF) d[i][j] = min(d[i][j], d[i][k] + d[k][j]); }
for (int k = 1; k <= n; k++) if (d[k][k] < 0) { cout << "NEGATIVE\n"; return 0; } // ✗ 没问 s 走不走得到
for (int i = 1; i <= n; i++) { if (d[s][i] >= INF) cout << "x"; else cout << d[s][i]; cout << " \n"[i == n]; } return 0;}点「运行 ▶」看结果
也输出 NEGATIVE —— 但原因和上一份完全不同:它看见了那个负环,
只是没问一句「s 过得去吗」。少的就是半句 && d[s][k] < INF。
★ 它错的不是算法,是题面。而 Bellman-Ford / SPFA 那两份天生不会犯这个错 —— 这就是第 1 步那张表想说的事。
9★ 对拍与生成器:它要同时满足四件互相打架的事
// ★ Bellman-Ford —— 负权的正经办法,也是这一章第二个关键一步//// 和 brute.cpp(Floyd)解同一道题,答案必须一模一样。//// ★ 一句话:**把所有边挨个松弛一遍,叫一轮;跑 n−1 轮就够了。**//// 为什么是 n−1,而且一轮不多一轮不少 —— 关键在这个不变量://// 跑完第 i 轮之后,dist[v] ≤「从 s 出发、**最多走 i 条边**能到 v 的最短长度」//// (归纳一行就完了:第 i 轮松弛边 (u,v) 时,dist[u] 已经不差于「最多 i−1 条边」的答案。)//// 而**没有负环时,最短路一定是一条简单路径** —— 它最多经过 n 个点、也就是 n−1 条边。// 所以 n−1 轮之后一定收敛。★ 「n−1」不是背下来的,是「简单路径最多 n−1 条边」这句话。//// ★ 判负环是白送的(第 31 章「判环白送」的第二次)://// 跑完 n−1 轮之后,**再跑一轮**。要是还有边能松弛成功 —— 有负环。//// 为什么:能松弛成功说明存在一条「用了 n 条边还更短」的走法,// n 条边的走法必然重复经过某个点,也就是绕了一个环,而绕它让距离变小 → 那是负环。//// ⚠ 而且这份代码天生只报告「**从 s 能走到的**负环」:// dist 是从 s 初始化的,走不到的点永远是 INF,`if (dist[u] == INF) continue;` 会把它挡住。// ★ 对照 brute.cpp:Floyd 看的是全图,那个过滤必须自己补一句 `d[s][k] < INF`。// **同一个题面,两种算法一个天生满足、一个必须手动补** —— 这种地方最容易写出不一致。//// ⚠ `if (dist[u] == INF) continue;` 这一句在**负权图上是必须的**,不是保险:// `INF + (-7)` 比 INF 小,不挡住的话,走不到的点会被刷出一个「比 INF 小一点」的距离,// 而且顺着边传染。第 32 章那个「以为会传染、实测不会」的坑,在这一章**真的会**发生 ——// 因为那时边权非负。**同一句代码危不危险,取决于数据的取值范围。**//// 复杂度 O(nm):n 轮,每轮扫 m 条边。比 Floyd 的 O(n³) 好在稀疏图上。
#include <bits/stdc++.h>using namespace std;
const int INF = 0x3f3f3f3f;
struct E { int u, v, w; };
int main() { int n, m, s; if (!(cin >> n >> m >> s)) return 0; vector<E> es(m); for (auto& e : es) cin >> e.u >> e.v >> e.w;
vector<int> dist(n + 1, INF); dist[s] = 0;
// ★ n−1 轮就够了:没有负环时,最短路是简单路径,最多 n−1 条边 for (int round = 1; round <= n - 1; round++) { bool changed = false; for (auto [u, v, w] : es) { if (dist[u] == INF) continue; // ⚠ 负权下这一句是必须的,见上面 if (dist[u] + w < dist[v]) { dist[v] = dist[u] + w; changed = true; } } if (!changed) break; // 提前收敛了就不用跑满(不影响正确性) }
// ★ 第 n 轮:还能松弛成功 ⇔ 从 s 能走到一个负环 for (auto [u, v, w] : es) { if (dist[u] == INF) continue; if (dist[u] + w < dist[v]) { cout << "NEGATIVE\n"; return 0; } }
for (int i = 1; i <= n; i++) { if (dist[i] >= INF) cout << "x"; else cout << dist[i]; cout << " \n"[i == n]; } return 0;}300 轮实测,五个错误版本:
⚠ 这里的「轮」是对拍的第几组随机数据,和 Bellman-Ford 那个「跑了几轮松弛」没有半点关系 —— 这一章里「轮」这个字被两件事共用了,看表时先认一眼主语。
| 故意写错的地方 | 被抓 | 第几组数据抓到 |
|---|---|---|
| SPFA 出队忘了清 inq | 135 / 300 | 第 1 轮 |
| Floyd 的 k 放到最内层 | 85 / 300 | 第 11 轮 |
| 不判「走不到」就松弛 | 85 / 300 | 第 3 轮 |
| Bellman-Ford 只跑 n−2 轮 | 71 / 300 | 第 4 轮 |
| 判负环不看 s 走不走得到 | 47 / 300 | 第 7 轮 |
(这 300 轮里,62 轮的答案是 NEGATIVE、141 轮有走不到的点,
4618 条边里 1294 条是负的。三个数字都钉在 check:viz 里。)
| 要什么 | 为了打假谁 |
|---|---|
| ① 负权边 | 没有它,这一章讲的东西一件都验不到(退化成第 32 章) |
| ② 从 s 走不到的点 | 「不判可达就松弛」(第 30、32 章那条第三次) |
| ③ 从 s 走不到的负环 | 「判负环不看可达」—— 本章最刁的一条 |
| ④ 长链 | 「Floyd 的 k 放错层」和「少跑一轮」:随机图上最短路两三条边就走完了 |
⚠ 而它们互相打架:
- 负权边一随机,就很容易撞出负环,那一组的答案就成了
NEGATIVE,长链白造了; - 负环一多,一大半数据的答案都是 NEGATIVE,别的 bug 全被挤没 (第 31、32 章「某一支占得太多」的第三次,档位 4 那一行就是现场);
- 图一碎(为了造走不到的点),长链就不容易连起来。
★ 解法是把它们拆开分别控制,两招都是从前面章节搬来的:
招一:势函数(第 32 章那个)。 造边时先随机一个非负的 w0,再令
w(u → v) = w0 + h[u] − h[v]任何环上 h 首尾相消 → 环长 = Σw0 ≥ 0 → 绝不可能有负环,但单条边可以是负的。
于是负环只在我故意注入的时候才出现(往回加一条特意配平过头的边)。
负权边的浓度归势函数管,负环的比例归注入管,两件事解耦了。
招二:把点分成两块。
R = id[0 .. L-1](起点 s 就在里面) U = id[L .. n-1]边只允许 R→R、U→U、U→R,永远不连 R→U —— 于是 U 天生从 s 走不到。
两块各串一条链保证内部连通,负环想注入哪一块都行。
把负环注入到 U 里,它就是「一个 s 永远走不到的负环」 —— 第 ③ 件事有了。
gen.cpp 带了十个档位(./gen 种子 档位),种子固定 1..300:
| 档位 | 改了什么 | NEGATIVE | 有 x | k 放错层 | 少跑一轮 | 不判可达 | 不看可达 | inq 没清 |
|---|---|---|---|---|---|---|---|---|
| 0(最初) | 非负权 + 处处可达 + 纯随机边 | 0 | 0 | 83 | 2 | 0 | 0 | 12 |
| 1 | 用势函数 → 出现负权边 | 0 | 0 | 83 | 2 | 0 | 0 | 12 |
| 2 | L 可以小于 n → 出现走不到的点 | 0 | 239 | 31 | 0 | 97 | 0 | 4 |
| 3 | R 那条链的 w0 全压成 0 → 逼出长最短路 | 0 | 239 | 73 | 4 | 97 | 0 | 69 |
| 4 | 注入负环(只往 R 里注) | 300 | 0 | 1 | 0 | 0 | 0 | 284 |
| 5 | 负环也可能注入进 U | 241 | 59 | 11 | 0 | 59 | 59 | 233 |
⚠ 档位 1 一个数字都没变。 光有负权边、没有别的配套,五个 bug 一个都没多抓到 —— 这条老实账要写在这儿:「加了个好东西」不等于「数据变好了」。
★ 档位 4 那一行是这一章最刺眼的地方:300 轮全是 NEGATIVE, 于是「k 放错层」从 73 掉到 1、「不判可达」从 97 掉到 0。 负环那一支占满了整张表,别的 bug 连出场的机会都没有。
| 档位 | 改了什么 | NEGATIVE | 有 x | k 放错层 | 少跑一轮 | 不判可达 | 不看可达 | inq 没清 |
|---|---|---|---|---|---|---|---|---|
| 5 | (上一张表的最后一行) | 241 | 59 | 11 | 0 | 59 | 59 | 233 |
| 6 | 注入比例降到 2/3 | 133 | 152 | 33 | 0 | 113 | 77 | 148 |
| 7 | ★ 最坏边序 + 链拉满全图 | 152 | 98 | 48 | 37 | 71 | 57 | 170 |
| 8 | 注入比例再降到 1/3 | 74 | 129 | 84 | 71 | 73 | 35 | 145 |
| 9(在用) | 注入尽量往 U 里放 | 62 | 141 | 85 | 71 | 85 | 47 | 135 |
★ 档位 7 那一行是这一章最费劲的一处,而且它教了一条新东西:
「少跑一轮」这个 bug,要现形得同时满足两件事 —— 最短路真的用满 n−1 条边(链得跨过所有点),而且边是按最坏顺序给的。
实测:只把边打乱 → 0 / 300;只把链拉满 → 4 / 300;两样一起 → 37 / 300。
⚠ 为什么边序这么要命:Bellman-Ford 一轮里是按输入顺序挨个松弛边的。 链要是正好顺着排,一轮就能从头传到尾,n−2 轮绰绰有余。 随机顺序下一轮平均也能往前推两三格。 只有把链倒着放,才逼得它一轮只推进一格 —— ★ 「n−1 轮」这个下界,只在最坏的边顺序下才是紧的。
⚠ 由此得到一条以前没写过的规矩:边的顺序也是数据的一部分。 而且更一般地:要证明一个下界是紧的,就得自己造出那个最坏情况 —— 随机数据永远碰不到它。这是随机对拍的又一个盲区 (第 20 章「对拍只能证伪」、第 31 章「验证器的盲区」之后的第三个)。
档位 9 的选法照旧是第 32 章那条:让最弱的那一支尽量强 (档位 8 最弱的是 35,档位 9 是 47)。
10实测:三个算法到底差多少
本机实测(./genBig <n>,稀疏图 m ≈ 4n,只能在终端里跑)。
⚠ 这张表的档位和第 7 步那次工作量统计不是同一组数据(那次固定在 ./genBig 2000,
这里最接近的一行是 2 400)—— 两张表要横着对的是形状,别拿某一行的秒数去除某一行的次数。
g++ -O2 -std=c++17 -o genBig genBig.cpp && g++ -O2 -std=c++17 -o brute brute.cpp
g++ -O2 -std=c++17 -o bell bell.cpp && g++ -O2 -std=c++17 -o spfa spfa.cpp
./genBig 2400 > big.txt
time ./brute < big.txt > /dev/null # 5.77 秒
time ./bell < big.txt > /dev/null # 0.00 秒
| 点数 n | 边数 | Floyd O(n³) |
Bellman-Ford O(nm) |
SPFA |
|---|---|---|---|---|
| 300 | 1 199 | 0.01 秒 | 0.00 秒 | 0.00 秒 |
| 600 | 2 399 | 0.09 秒 | 0.00 秒 | 0.00 秒 |
| 1 200 | 4 799 | 0.69 秒 | 0.00 秒 | 0.00 秒 |
| 2 400 | 9 599 | 5.77 秒 | 0.00 秒 | 0.00 秒 |
| 20 000 | 79 999 | 跑不动 | 0.04 秒(11 轮) | 0.05 秒 |
| 80 000 | 319 999 | 跑不动 | 0.19 秒(13 轮) | 0.23 秒 |
| 320 000 | 1 279 999 | 跑不动 | 0.87 秒(15 轮) | 1.37 秒 |
★ Floyd 的 n³ 一点折扣都不打:n 翻一倍,它慢八倍(0.09 → 0.69 → 5.77)。
它算的是所有点对,这道题只要一行 —— 单源问题上别用它。
第 7 步刚量过:SPFA 的松弛尝试只有 Bellman-Ford 的 0.19 倍。可它跑得更慢(1.37 vs 0.87 秒)。
两个原因,都能查出来:
- Bellman-Ford 根本没跑满 n−1 轮。 那句
if (!changed) break;让它在随机图上 15 轮就收敛了(n = 320 000!)。所谓O(nm)的 n,实际只有 15。 - 常数不一样。 Bellman-Ford 的内层是在一个连续数组上顺序扫边,缓存友好到极点; SPFA 要维护队列、还要顺着邻接表跳来跳去。 (第 29 章那条「链式前向星常数最小在稠密图上不成立,原因是缓存」的同款。)
★ 「SPFA 比 Bellman-Ford 快」也是一句要复核的口诀。 少做的工作是真的,可它换来的收益被常数吃掉了。
第二笔账:我本来还想造一组「卡 SPFA」的数据(一般说的就是网格图),把它的最坏情况演出来。 没做成:2500 个点、9800 条边的随机权网格上,SPFA 只入队了 5188 次(约 2n), 一点都没退化。
「SPFA 已死」是真的 —— 它的最坏复杂度确实还是
O(nm)。 但光把图摆成网格不够,要卡住它得针对边权专门构造。这一章没做出来,如实写在这里。 (第 29、32 章那条「口诀要拿实测复核」的第三次 —— 而这一次连「口诀是对的,只是我没复现出来」都得说清楚。)
11三种算法怎么选
| 复杂度 | 负权 | 负环 | 什么时候用它 | |
|---|---|---|---|---|
| Dijkstra(第 32 章) | O(m log n) |
✗ | ✗ | 边权非负 —— 能用它就用它,快一个数量级 |
| Floyd | O(n³) |
✓ | ✓(全图) | 要所有点对之间的距离,而且 n 很小(几百) |
| Bellman-Ford | O(nm) |
✓ | ✓(s 可达) | 有负权 / 要判负环;代码最短,而且常数极小 |
| SPFA | 最坏 O(nm) |
✓ | ✓(s 可达) | 同上,通常更快 —— 但最坏情况会被卡 |
★ 一句话:先问边权有没有负数。 没有就 Dijkstra,有就 Bellman-Ford 那一族; 要所有点对、而且 n 小,才轮到 Floyd。
12这一章可以带走的五样东西
【1】★ Floyd 的 k 是阶段,必须在最外层。
f[k][i][j] = min(f[k-1][i][j], f[k-1][i][k] + f[k-1][k][j]) ——
k 是阶段,i、j 只是格子。这就是第 21 章「依赖谁就先填谁」的第七次登场,
也和第 23 章「转移右边的第一维必须是 i−1」是同一个形状。
⚠ 放错层不崩溃、不报错,300 轮里只错 85 轮。
【2】★ Bellman-Ford 的 n−1 不是背的。 跑完第 i 轮,dist 就不差于「最多走 i 条边」的答案; 而没有负环时最短路是简单路径,最多 n−1 条边。判负环是白送的:多跑一轮还能松弛就有环。 ⚠ 少跑一轮不是答案错,是误报负环(没收敛而已)。
【3】★ SPFA 就是第 32 章那份「不小心答对了」的堆优化。
同一个算法,只差用什么容器。所以上一章那句结论要收尾成:
堆优化在负权图上答对了,不是因为 Dijkstra 对负权成立,而是因为它执行的是 SPFA。
⚠ inq 记的是「在不在队里」,出队要清 —— 别和第 30 章 BFS 的 vis 记混。
【4】★ 同一句代码危不危险,取决于数据的取值范围。
「不判可达就松弛」在第 32 章(非负权)里完全没事,在这一章会一路传染、还连带误报负环。
同理,「走不到」这一章不能再用 -1 当记号 —— 因为距离本身可以是负的。
没有脱离数据的代码审查。
【5】★ 边的顺序也是数据的一部分。 「n−1 轮」这个下界只在最坏的边顺序下才紧:只打乱边 → 0 / 300;只把链拉满 → 4 / 300; 两样一起才 37 / 300。 更一般地:要证明一个下界是紧的,就得自己造出那个最坏情况 —— 随机数据永远碰不到它。这是随机对拍的第三个盲区 (前两个:第 20 章「只能证伪」、第 31 章「验证器证明不了没漏报」)。
第 34 章:最小生成树 —— Kruskal 与 Prim。
从「两点之间最短」换成「把所有点连起来,总代价最小」。 ★ 关键一步是切割性质:横跨任意一个切割的最小边,一定在某棵最小生成树里 —— Kruskal 和 Prim 都是它的推论,只是「怎么选那个切割」不一样。
顺带一个这一章埋下的坑:两种算法给出的树可能长得不一样,但权值和必须相同 —— 那么对拍该比什么?(第 31 章「答案不唯一」的第二次登场。)
「走不到输出 x」是这一章为了把话说清楚才定的记号,真题不这么写:
- 洛谷的单源最短路模板(P3371 / P4779)要求走不到的点输出 2147483647;
- 判负环那一类(P3385、P2850)问的干脆是 YES / NO,一个距离都不用输出。
⇒ 算法一个字都不用改,改的是输出那几行。 交题前先把题面里 「走不到怎么输出」「多组数据要不要清空」这两句找出来 —— 这正是第 1 步那条「答案的记号和数据的取值范围是一对」的考场版本。
13自测
- 洛谷 B3647 【模板】Floyd解析 → —— 本章模板题。写完对着 brute.cpp 逐行检查三重循环的顺序
- 洛谷 P3385 【模板】负环解析 → —— ★ 判负环模板。注意它问的是「从 1 出发能不能走到负环」—— 正是本章第 1 步那件事
- 洛谷 P1119 灾后重建解析 → —— ★ Floyd 的神题:村庄按时间一个个修好,正好就是「k 一层一层加进去」。做完你会真的懂 k 为什么在最外层
- 洛谷 P2865 [USACO06NOV] Roadblocks G解析 → —— 次短路。把「最短」拆成两个状态,松弛的写法要改一改
- 洛谷 P1266 速度限制解析 → —— 进阶:状态里要带上「当前速度」——「状态是什么」这件事比算法本身难
- 洛谷 P2850 [USACO06DEC] Wormholes G解析 → —— 虫洞 = 负权边,问有没有负环。多组数据,注意每组都要清干净