0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 B3647,日期见页头。两边不一致时信原站。
题目描述
给出一张由 n 个点 m 条边组成的无向连通图。
求出所有点对 (i, j) 之间的最短路径。
输入格式
第一行为两个整数 n, m,分别代表点的个数和边的条数。
接下来 m 行,每行三个整数 u, v, w,代表 u, v 之间存在一条边权为 w 的边。
输出格式
输出 n 行每行 n 个整数。
第 i 行的第 j 个整数代表从 i 到 j 的最短路径。
说明/提示
对于 100% 的数据,n ≤ 100,m ≤ 4500,任意一条边的权值 w 是正整数且 1 ≤ w ≤ 1000。
数据中可能存在重边。
时限 1 秒,内存 512 MB。
输入输出样例
输入
4 4 1 2 1 2 3 1 3 4 1 4 1 1
输出
0 1 2 1 1 0 1 2 2 1 0 1 1 2 1 0
一个四元环,每条边长 1。1 → 3 绕哪边都是 2,其余相邻的都是 1。
★ 注意对角线那四个 0 —— 它们不是算出来的,是初始化时手填的(第 ④ 步)。
1第一版:照着「试试从 k 中转」写出来的三重循环
「i 到 j 的最短路,要么直达,要么从某个 k 中转」—— 这句话翻成代码, 最顺手的写法是先定死 i 和 j,再把 k 扫一遍:
// ✗ 错法①:k 放到最内层 —— 三行一个字没改,只换了顺序//// 本章第 5 步在自己那张图上演过一次;这一页量的是它在**这道真题**上被抓多少。// ⚠ 它不崩溃、不报错,只在「最短路要拐好几个弯、而那些弯的编号顺序不巧」时才现形。
#include <bits/stdc++.h>using namespace std;const int INF = 0x3f3f3f3f;int d[105][105];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) d[i][j] = (i == j ? 0 : INF); for (int e = 0; e < m; e++) { int u, v, w; cin >> u >> v >> w; d[u][v] = min(d[u][v], w); d[v][u] = min(d[v][u], w); }
for (int i = 1; i <= n; i++) // ✗ i、j 在外,k 在里 for (int j = 1; j <= n; j++) for (int k = 1; k <= n; k++) d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) cout << d[i][j] << " \n"[j == n]; return 0;}点「运行 ▶」看结果
它在官方样例上打出的就是标准答案。 而它是错的。
外层定死 i、j 之后,内层用到的 d[i][k] 和 d[k][j] 里,
很多格子这时候压根还没被更新过 —— 拿半成品去算,结果就是半成品。
⚠ 而它有一个很容易骗过人的性质:编号顺着排的链上它是对的。
链 1 - 2 - 3 - … - n 上,算 d[1][j] 时 d[1][j−1] 恰好刚算完,一路顺推下去正好没错。
⇒ 顺手画一条链去试,试不出来。
| 300 轮 | 随机图 | ★ 长链 + 编号打乱 | 稠密图 |
|---|---|---|---|
| 「k 放到最内层」被抓 | 147 | ★ 235 | 117 |
★ 那一档只改了一件事:链的顺序和编号顺序脱钩。 最短路平均要拐的弯也跟着从 2.05 条边涨到 3.00 条边 —— ⇒ 「k 放错层」现形的条件是最短路真的要中转好几次,而随机小图上它两步就走完了。
2★ 关键的一步:k 是阶段,i 和 j 只是格子
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 是阶段,i、j 只是表格里的格子 ⇒ 阶段必须在最外层。
这就是第 21 章那句「依赖谁,就先填谁」,
也和第 23 章「01 背包转移右边的第一维必须是 i−1」是同一个形状。
完整的推导在本章第 3 步,这里只兑现那句话:把 k 提到最外层。
// 洛谷 B3647 【模板】Floyd —— ★ 这一版就能 AC//// n ≤ 100 ⇒ 三重循环 10⁶ 次,本机 1 毫秒。// 三处和「模板」二字有关的细节,每一处都在这一页有一个错误版本对照:// ① k 在最外层(本章第 5 步)// ② 无向图 ⇒ 每条边存两遍// ③ 重边 ⇒ 取 min,不能直接赋值// 外加一处这道题独有的:INF 用 0x3f3f3f3f,理由不是「够大」,是 **INF + INF 不溢出**。
#include <bits/stdc++.h>using namespace std;
const int INF = 0x3f3f3f3f; // 1 061 109 567,两个加起来 2 122 219 134 < 2³¹−1int d[105][105];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m;
for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) d[i][j] = (i == j ? 0 : INF); // ★ 对角线要手动置 0
for (int e = 0; e < m; e++) { int u, v, w; cin >> u >> v >> w; d[u][v] = min(d[u][v], w); // ★ 重边取 min(题面明写「可能存在重边」) d[v][u] = min(d[v][u], w); // ★ 无向图,存两遍 }
for (int k = 1; k <= n; k++) // ★ k 是阶段,必须在最外层 for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) d[i][j] = min(d[i][j], d[i][k] + d[k][j]);
for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) cout << d[i][j] << " \n"[j == n]; } return 0;}点「运行 ▶」看结果
n ≤ 100 ⇒ 三重循环 10⁶ 次,顶格本机 2.4 毫秒,时限 1 秒。模板题的正解就是它。
3★★★ INF 该写多大 —— 这道题换了一个主语
上一章那三道题(P3371 / P4779 / P1462) 把「INF 够不够大」量了个遍。这道题问的不是那个。
// ✗ 错法②:INF 写成 INT_MAX —— ★★★ 这一页的主线//// 上一章(P3371 / P4779 / P1462)问的是「INF 够不够大」,答案是拿题面乘一遍。// 这一章的 Floyd 换了一个主语:**INF + INF 会不会溢出**。//// Floyd 的内层是 `d[i][k] + d[k][j]`,而在中间那些层里,这两个格子**很可能都还是 INF**// (k 这一层还没把它们连起来)。INT_MAX + INT_MAX 在 int 里绕回成 −2 ⇒// min 会当场把这个「负距离」收下,然后顺着它一路传染出去。//// ⇒ 0x3f3f3f3f 的全部意义就在这儿:2 × 1 061 109 567 = 2 122 219 134 < 2 147 483 647。
#include <bits/stdc++.h>using namespace std;const int INF = INT_MAX; // ✗ 就改了这一行int d[105][105];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) d[i][j] = (i == j ? 0 : INF); for (int e = 0; e < m; e++) { int u, v, w; cin >> u >> v >> w; d[u][v] = min(d[u][v], w); d[v][u] = min(d[v][u], w); } for (int k = 1; k <= n; k++) for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) d[i][j] = min(d[i][j], d[i][k] + d[k][j]); // ✗ INT_MAX + INT_MAX 绕回成 −2 for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) cout << d[i][j] << " \n"[j == n]; return 0;}点「运行 ▶」看结果
先把「够不够大」那道算术题做完,一行就完了:
| 题面顶格时最长的最短路 | 99 × 1000 = 99 000 |
int 的上限 |
2 147 483 647 |
| ⇒ 余量 | ★ 21 692 倍 |
⇒ 随便挑一个大数当 INF 都「够大」。 可 INT_MAX 照样炸,因为 Floyd 的内层是
d[i][j] = min(d[i][j], d[i][k] + d[k][j]);而在中间那些层里,d[i][k] 和 d[k][j] 很可能都还是 INF(k 这一层还没把它们连起来)。
0x3f3f3f3f |
1 061 109 567,两个加起来 2 122 219 134 < 2 147 483 647 ✓ |
INT_MAX |
2 147 483 647,两个加起来在 int 里绕回成 −2 ✗ |
⇒ min 当场把这个「负距离」收下,然后顺着它一路传染 —— 样例上四个点就已经全烂了。
★★★
0x3f3f3f3f的全部意义就在这儿:它是「自己加自己还不溢出的最大整数」那一档。 上一章问的是它够不够大,这一章问的是它加倍会不会翻车 —— 同一个常量,换一个算法就换一个主语。
度量程序把三重循环里「两个加数至少有一个是 INF」的次数数了一遍(40 个点,各 30 张图):
| 稀疏图(m ≈ n) | 稠密图(接近完全图) | |
|---|---|---|
| 加数含 INF 的比例 | 244‰ | ★ 20‰ |
INT_MAX 那一版 300 轮被抓 |
287 | 256 |
⇒ 图越密,两点之间越早连上,中途撞见 INF 的机会就越少 —— 这个 bug 的抓获率跟着图的稀疏程度走。 ⚠ 而它在四个档位上都是 250 以上:这是个「几乎每组都错」的 bug,官方样例当场就挡住了。
4⚠ 另外两个「每组都错」的:无向和对角线
// ✗ 错法④:无向图只存一遍//// 「存一遍还是两遍」这一行,第 32 章那张题单里演了四遍、四次答案都不一样。// 这道题题面第一句就是「一张**无向**连通图」⇒ 必须两遍。
#include <bits/stdc++.h>using namespace std;const int INF = 0x3f3f3f3f;int d[105][105];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) d[i][j] = (i == j ? 0 : INF); for (int e = 0; e < m; e++) { 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++) for (int j = 1; j <= n; j++) d[i][j] = min(d[i][j], d[i][k] + d[k][j]); for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) cout << d[i][j] << " \n"[j == n]; return 0;}点「运行 ▶」看结果
| 题 | 题面怎么说 | 那一行 |
|---|---|---|
| P1339 | 无向图 | 必须写 |
| P1629 | 「所有的道路都是单行的」 | 写了当场就错 |
| P1462 | 「m 条双向的公路」 |
又必须写 |
| P1073 | 同一张输入里 z 说了算 |
一半写一半不写 |
| B3647(这道题) | 「一张无向连通图」 | 必须写 |
⇒ 它从来不是一个能背的习惯,是每道题读一次题面的事(第 52 章那条,第五次现场)。 ★ 而下一道 P3385 会把它推到更狠的地方:边权的正负号说了算。
// ✗ 错法⑤:对角线没置 0(整张表统一初始化成 INF)//// 题面要「第 i 行第 j 个整数代表从 i 到 j 的最短路径」——// i == j 那一格的答案是 **0**,而它不是 Floyd 算出来的,是初始化时手填的。
#include <bits/stdc++.h>using namespace std;const int INF = 0x3f3f3f3f;int d[105][105];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) d[i][j] = INF; // ✗ 少了 i == j 那一支 for (int e = 0; e < m; e++) { int u, v, w; cin >> u >> v >> w; d[u][v] = min(d[u][v], w); d[v][u] = min(d[v][u], w); } for (int k = 1; k <= n; k++) for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) d[i][j] = min(d[i][j], d[i][k] + d[k][j]); for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) cout << d[i][j] << " \n"[j == n]; return 0;}点「运行 ▶」看结果
题面要「第 i 行第 j 个整数代表从 i 到 j 的最短路径」,i == j 那一格的答案是 0。
它不是 Floyd 算出来的,是初始化时手填的 —— 少了那一支,d[i][i] 会被「绕出去再绕回来」填成一个正数。
★ 这两个都是 300 / 300(每组都错),官方样例两个都挡住了。
5⚠ 题面加粗的那句话:重边
// ✗ 错法③:重边直接赋值,没取 min//// 题面最后一行专门加粗写着「**数据中可能存在重边**」——// 而顺手写 `d[u][v] = w;` 的话,**后读进来的那条边会把先读进来的短边盖掉**。
#include <bits/stdc++.h>using namespace std;const int INF = 0x3f3f3f3f;int d[105][105];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) d[i][j] = (i == j ? 0 : INF); for (int e = 0; e < m; e++) { int u, v, w; cin >> u >> v >> w; d[u][v] = w; // ✗ 直接盖掉 d[v][u] = w; } for (int k = 1; k <= n; k++) for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) d[i][j] = min(d[i][j], d[i][k] + d[k][j]); for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) cout << d[i][j] << " \n"[j == n]; return 0;}点「运行 ▶」看结果
题面最后一行专门加粗写着「数据中可能存在重边」。
顺手写 d[u][v] = w; 的话,后读进来的那条边会把先读进来的短边盖掉。
| 300 轮 | 随机图(顺手就有重边) | ★ 重边档 | 稠密图 |
|---|---|---|---|
| 「重边不取 min」被抓 | 191 | ★ 223 | 289 |
★ 重边档只做了一件事:往同一对点上压两三条边,权值差一个数量级(15 或 200500)。
⚠ 而稠密档抓得更多(273)—— 因为随机撒边本来就会撞出大量重边。
| 错法 | 300 轮被抓 | 官方样例挡住了吗 |
|---|---|---|
| INF 写成 INT_MAX | 287 | ★ 挡住了 |
| 无向图只存一遍 | 300 | ★ 挡住了 |
| 对角线没置 0 | 300 | ★ 挡住了 |
| k 放到最内层 | 147 | ⚠ 放过了 |
| 重边不取 min | 191 | ⚠ 放过了 |
⇒ 又一次那条规律:样例筛掉的是「每组都错」的,放过的是「偶尔才错」的 —— 而真正让你 WA 在第 7 个点上的,恰恰是后一种。
6★★ 换一条路:跑 n 次 Dijkstra —— 而本章正文那句话在这里要反过来说
// ★ 另一条路:跑 n 次堆优化 Dijkstra(第 32 章那份)//// 它和 Floyd 一行代码都不共享 —— 所以它既是对拍的参照物,也是「该选哪个」那张表的另一半。// 复杂度 O(n × m log m):n = 100、m = 4500 ⇒ 约 5.4 × 10⁶,而 Floyd 是 n³ = 10⁶。// ⚠ 本章正文说「单源问题上别用 Floyd」,这道题问的是**全源** —— 结论正好反过来。
#include <bits/stdc++.h>using namespace std;const int INF = 0x3f3f3f3f;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; vector<vector<pair<int, int>>> g(n + 1); for (int e = 0; e < m; e++) { int u, v, w; cin >> u >> v >> w; g[u].push_back({v, w}); g[v].push_back({u, w}); // 无向图,两遍 } for (int s = 1; s <= n; s++) { vector<int> dist(n + 1, INF); priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> q; dist[s] = 0; q.push({0, s}); while (!q.empty()) { 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}); } } for (int j = 1; j <= n; j++) cout << dist[j] << " \n"[j == n]; } return 0;}点「运行 ▶」看结果
顶格 n = 100、m = 4500 |
数次数(机器无关) | 本机秒表(A 机 · WSL2 · 2026-08-31,含读入和输出 10 000 个数) |
|---|---|---|
Floyd O(n³) |
三重循环 1 000 000 次 | ★ 2.4 毫秒 |
n 次堆优化 Dijkstra O(n × m log m) |
松弛尝试 900 000 次 | 4.8 毫秒 |
★ 两把尺子又打架了:Dijkstra 的次数还少一点(0.9 倍),秒表却慢一倍。 理由和 P2853 那次同款:Floyd 的内层是在一个连续的二维数组上顺序扫, 对 cache 和向量化都友好到极点;Dijkstra 要维护堆、还要顺着邻接表跳来跳去。
⇒ 这是「次数和秒表量的从来不是同一件事」的第四次现场 (前三次:P1074 次数一样秒表差 9.5 倍、P2925 差 1.13 倍、 P2853 次数差 100 倍秒表差 14 倍)。 ★ 而这一次的方向最干净:次数少的那个反而慢。
★ 一句话:「Floyd 慢」的主语是「单源」。 要的是所有点对、而且 n 只有一百,它是最短、最快、最不容易写错的那一版。
7★ 对拍这一页
参照物是枚举所有简单路径:既不是 Floyd 也不是 Dijkstra,直接照「最短路」三个字的定义走一遍。
300 轮(n 随机 5~8) |
随机图 | ★ 长链打乱档 | ★ 重边档 | 稠密档 |
|---|---|---|---|---|
| Floyd ≡ 枚举所有路径 | ★ 0 | ★ 0 | ★ 0 | ★ 0 |
| n 次 Dijkstra ≡ 枚举所有路径 | ★ 0 | ★ 0 | ★ 0 | ★ 0 |
| k 放到最内层 | 147 | ★ 235 | 160 | 117 |
| INF 写成 INT_MAX | 287 | 300 | 285 | ★ 256 |
| 重边不取 min | 191 | 112 | ★ 223 | 289 |
| 无向图只存一遍 | 300 | 300 | 300 | 300 |
| 对角线没置 0 | 300 | 300 | 300 | 300 |
8度量程序和生成器
9一页纸
| ★ 关键的一步 | k 是阶段,必须在最外层(本章第 3 步那个三维 DP) |
| ⚠ 放错层 | 随机图抓 147;★ 编号顺着排的链上它是对的 ⇒ 长链要打乱编号才抓到 235 |
| ★★★ INF 该写多大 | 这道题换了主语:够不够大余量 21 692 倍,真正的问题是 INF + INF 溢不溢出 ⇒ 0x3f3f3f3f |
| ⚠ 无向 / 对角线 | 两个 300 / 300,官方样例都挡住了 |
| ⚠ 重边 | 题面加粗写着,而样例里没有重边 ⇒ 放过了(随机图 191、重边档 223) |
| ★★ 全源用哪个 | Floyd 2.4 毫秒 vs n 次 Dijkstra 4.8 毫秒 —— 次数少的那个反而慢一倍 |