题单 · 习题解析

洛谷 B3647 【模板】Floyd

★ k 是**阶段**必须在最外层(本章第 3 步那个三维 DP)—— ⚠ 而**编号顺着排的链上放错层也是对的**,长链要**打乱编号**才从 147 抓到 235;★★★ 「INF 该写多大」在这道题**换了主语**:够不够大余量 21 692 倍,真正的问题是 `INF + INF` 溢不溢出 ⇒ `0x3f3f3f3f` 的全部意义就是「自己加自己还不溢出」;★★ 全源问题上 **Floyd 2.4 毫秒 vs n 次 Dijkstra 4.8 毫秒** —— 次数少的那个反而慢一倍(两把尺子打架的第四次)

原题:洛谷 B3647出自 第 33 章 最短路二:Floyd、Bellman-Ford、SPFA 与负环 的题单题面本地存档:2026-08-31
⚠ 先自己写一遍,再往下看

这一页不是标准答案,是一条阶梯:把大多数人真实会写出的第一版摆出来, 看它错在哪、慢在哪,再一步一步改。所以它对「已经自己动手撞过一次墙」的人最有用。
还没写过的话,先去写 —— 写出来的哪怕是错的,也比直接看这一页收获大。

0题目原文(本地存了一份)

为什么要在这儿抄一份题面

原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。

转录自洛谷 B3647,日期见页头。两边不一致时信原站。

题目描述

给出一张由 n 个点 m 条边组成的无向连通图

求出所有点对 (i, j) 之间的最短路径。

输入格式

第一行为两个整数 n, m,分别代表点的个数和边的条数。

接下来 m 行,每行三个整数 u, v, w,代表 u, v 之间存在一条边权为 w 的边。

输出格式

输出 n 行每行 n 个整数。

i 行的第 j 个整数代表从 ij 的最短路径。

说明/提示

对于 100% 的数据,n ≤ 100m ≤ 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 扫一遍

b3647K.cpp✗ for (i) for (j) for (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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

它在官方样例上打出的就是标准答案。 而它是错的。

⚠ 为什么放错层就错了 —— 本章第 5 步那件事,这里是它在真题上的价钱

外层定死 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 只是格子

★ 三行循环底下是一个三维 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 是阶段,i、j 只是表格里的格子 ⇒ 阶段必须在最外层。 这就是第 21 章那句「依赖谁,就先填谁」, 也和第 23 章「01 背包转移右边的第一维必须是 i−1」是同一个形状。

完整的推导在本章第 3 步,这里只兑现那句话:把 k 提到最外层

b3647.cpp★ 这一版就能 AC(Floyd,本机 2.4 毫秒)
// 洛谷 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³¹−1
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); // ★ 对角线要手动置 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

n ≤ 100 ⇒ 三重循环 10⁶ 次,顶格本机 2.4 毫秒,时限 1 秒。模板题的正解就是它。

3★★★ INF 该写多大 —— 这道题换了一个主语

上一章那三道题(P3371 / P4779 / P1462) 把「INF 够不够大」量了个遍。这道题问的不是那个。

b3647IntMax.cpp✗ INF 写成 INT_MAX(样例当场打出 −2147483647)
// ✗ 错法②: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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★★ 「够不够大」在这道题上根本不成立 —— 真正的问题是「加起来会不会溢出」

先把「够不够大」那道算术题做完,一行就完了:

题面顶格时最长的最短路 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」是能数出来的,而且它解释了一个对照档

度量程序把三重循环里「两个加数至少有一个是 INF」的次数数了一遍(40 个点,各 30 张图):

稀疏图(m ≈ n) 稠密图(接近完全图)
加数含 INF 的比例 244‰ 20‰
INT_MAX 那一版 300 轮被抓 287 256

⇒ 图越密,两点之间越早连上,中途撞见 INF 的机会就越少 —— 这个 bug 的抓获率跟着图的稀疏程度走。 ⚠ 而它在四个档位上都是 250 以上:这是个「几乎每组都错」的 bug,官方样例当场就挡住了。

4⚠ 另外两个「每组都错」的:无向和对角线

b3647Dir.cpp✗ 无向图只存一遍(样例打出 0 1 2 3 / 3 0 1 2 …)
// ✗ 错法④:无向图只存一遍
//
// 「存一遍还是两遍」这一行,第 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★ 「存一遍还是两遍」演到了第五遍 —— 而这次是最简单的那种
题面怎么说 那一行
P1339 无向图 必须写
P1629 「所有的道路都是单行的」 写了当场就错
P1462 m双向的公路」 又必须写
P1073 同一张输入里 z 说了算 一半写一半不写
B3647(这道题) 「一张无向连通图」 必须写

⇒ 它从来不是一个能背的习惯,是每道题读一次题面的事(第 52 章那条,第五次现场)。 ★ 而下一道 P3385 会把它推到更狠的地方:边权的正负号说了算。

b3647Diag.cpp✗ 整张表都初始化成 INF,对角线没置 0(样例打出 2 1 2 1 …)
// ✗ 错法⑤:对角线没置 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

题面要「第 i 行第 j 个整数代表从 i 到 j 的最短路径」,i == j 那一格的答案是 0。 它不是 Floyd 算出来的,是初始化时手填的 —— 少了那一支,d[i][i] 会被「绕出去再绕回来」填成一个正数。

★ 这两个都是 300 / 300(每组都错),官方样例两个都挡住了

5⚠ 题面加粗的那句话:重边

b3647Dup.cpp✗ 重边直接赋值(样例照过 —— 那组数据没有重边)
// ✗ 错法③:重边直接赋值,没取 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

题面最后一行专门加粗写着「数据中可能存在重边」。 顺手写 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 —— 而本章正文那句话在这里要反过来说

b3647Dij.cpp★ n 次堆优化 Dijkstra —— 也能 AC,而且慢一倍
// ★ 另一条路:跑 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★★ 本章正文说「单源问题上别用 Floyd」—— 这道题问的是全源,结论正好翻过来
顶格 n = 100m = 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,直接照「最短路」三个字的定义走一遍。

b3647Brute.cpp参照物:枚举所有简单路径(300 轮不一致 0 轮)
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度量程序和生成器

b3647Count.cpp度量程序(本页所有数字都出自它)
b3647Gen.cpp(五个档位)数据生成器

9一页纸

★ 关键的一步 k 是阶段,必须在最外层(本章第 3 步那个三维 DP)
⚠ 放错层 随机图抓 147;★ 编号顺着排的链上它是对的 ⇒ 长链要打乱编号才抓到 235
★★★ INF 该写多大 这道题换了主语:够不够大余量 21 692 倍,真正的问题是 INF + INF 溢不溢出 ⇒ 0x3f3f3f3f
⚠ 无向 / 对角线 两个 300 / 300,官方样例都挡住了
⚠ 重边 题面加粗写着,而样例里没有重边 ⇒ 放过了(随机图 191、重边档 223)
★★ 全源用哪个 Floyd 2.4 毫秒 vs n 次 Dijkstra 4.8 毫秒 —— 次数少的那个反而慢一倍