0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P3371,日期见页头。两边不一致时信原站。
题目背景
本题测试数据为随机数据,在考试中可能会出现构造数据让 SPFA 不通过, 如有需要请移步 P4779。
题目描述
如题,给出一个有向图,请输出从某一点出发到所有点的最短路径长度。
输入格式
第一行包含三个整数 n, m, s,分别表示点的个数、有向边的个数、出发点的编号。
接下来 m 行每行包含三个整数 u, v, w,表示一条 u → v 的,长度为 w 的边。
输出格式
输出一行 n 个整数,第 i 个表示 s 到第 i 个点的最短路径,
若不能到达则输出 2³¹ − 1。
说明/提示
【数据范围】
对于 20% 的数据:1 ≤ n ≤ 5,1 ≤ m ≤ 15;
对于 40% 的数据:1 ≤ n ≤ 100,1 ≤ m ≤ 10⁴;
对于 70% 的数据:1 ≤ n ≤ 1000,1 ≤ m ≤ 10⁵;
对于 100% 的数据:1 ≤ n ≤ 10⁴,1 ≤ m ≤ 5 × 10⁵,1 ≤ u, v ≤ n,
w ≥ 0,Σw < 2³¹,保证数据随机。
Update 2022/07/29:两个点之间可能有多条边,敬请注意。
对于真正 100% 的数据,请移步 P4779。 请注意,该题与本题数据范围略有不同。
样例说明:

(原站附注:图片 1 到 3 和 1 到 4 的文字位置调换。)
时限 1 秒,内存 128 MB。
输入输出样例
输入
4 6 1 1 2 2 2 3 2 2 4 1 1 3 5 3 4 3 1 4 4
输出
0 2 4 3
起点是 1 号。到 2 号走 1→2 是 2;到 3 号走 1→2→3 是 4(比直接那条长 5 的边短);
到 4 号走 1→2→4 是 3(比直接那条长 4 的边短)。
⇒ 两次「绕一下反而更近」 —— 正是本章第 2 步手算那张图里的那件事。
1★ 关键的一步:本章那三句话,一个字都不用改
每次取「当前最近的、还没定下来的点」,它的距离当场定死;再拿它去松弛邻居。 本章第 5 步把这个贪心连同三句话的反证一起写完了,这里一个字不用改。
⇒ 所以这一页从第 ② 步开始,讲的全是「算法之外的东西」 —— 而这道题的四个错法,没有一个错在 Dijkstra 上。
// P3371【模板】单源最短路径(弱化版)—— ★ 这一版就能 AC//// 算法和本章 naive.cpp 一模一样(朴素 O(n²) Dijkstra,那三句话一个字没改)。// **这一页真正要讲的,是它和本章那份代码之间那两处不起眼的差别。**//// ① 输出格式换了主语:本章的题面写「走不到输出 -1」,// 这道题写的是「**若不能到达则输出 2^31−1**」。这一条对拍抓不到(两份都错就都错),// 只能一个字一个字对着题面读。//// ② ★★★ 而真正会咬人的是「Σw < 2^31」这一句 ——// 它允许一条最短路长到 **2 147 483 647**,// 而本章 naive.cpp 里那个 `INF = 0x3f3f3f3f` 只有 **1 061 109 567**。// ⇒ 距离一旦越过 0x3f3f3f3f,那份代码会把一个**走得到**的点当成走不到的(见 p3371Inf3f.cpp)。// ⇒ 这里 `dist` 用 long long、INF 用一个真正够大的数,两个坑一起躲开。// (★「顶格是顶到**题面**的边上,不是顶到类型的边上」—— 第 9 章那条的又一次。)//// ③ 顺带一条:题面 2022/07/29 的 Update 专门提醒「两个点之间可能有多条边」。// ⚠ 对**邻接表**这种存法,这句话是**噪声** —— 重边原样躺在 g[u] 里,// 松弛时那个 `<` 自己会挑短的。它只对「邻接矩阵直接赋值」那种写法是命门(p3371Mat.cpp)。//// 复杂度 O(n² + m):顶格 n = 10⁴、m = 5×10⁵ ⇒ 10⁸ + 5×10⁵,本机实测 0.11 秒(时限 1 秒)。// ⚠ 但「朴素能过」不等于「可以用邻接矩阵」:10⁴ × 10⁴ 个 int 是 400 MB,题面只给 128 MB。
#include <bits/stdc++.h>using namespace std;typedef long long ll;
const ll INF = (ll)4e18;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
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<ll> dist(n + 1, INF); vector<int> vis(n + 1, 0); dist[s] = 0;
for (int step = 0; step < n; step++) { int u = -1; for (int j = 1; j <= n; j++) if (!vis[j] && (u == -1 || dist[j] < dist[u])) u = j; if (u == -1 || dist[u] == INF) break; // 剩下的都走不到了 vis[u] = 1; for (auto [v, w] : g[u]) if (dist[u] + w < dist[v]) dist[v] = dist[u] + w; }
const ll UNREACH = 2147483647LL; // ★ 2^31 − 1,题面指定的哨兵值 for (int i = 1; i <= n; i++) cout << (dist[i] == INF ? UNREACH : dist[i]) << " \n"[i == n]; return 0;}点「运行 ▶」看结果
2⚠ 先数一数:题面把哪几个主语换掉了
本章的题面和这道题的题面,看着几乎一样。一行一行比过去,换了三处:
| 本章题面 | 这道题 | |
|---|---|---|
| 走不到的点 | 输出 -1 |
★ 输出 2³¹ − 1 |
| 边长 | 1 ≤ w ≤ 100 |
★ w ≥ 0,而且只约束了总和:Σw < 2³¹ |
| 重边 | 「可能有」 | 「可能有」+ 2022 年专门补了一句 Update |
Σw < 2³¹ 意味着一条最短路可以长到 2 147 483 647。
而本章 naive.cpp 里那个习惯写法是 const int INF = 0x3f3f3f3f; —— 它只有 1 061 109 567。
⇒ 两者差 2.02 倍。 距离一旦越过 0x3f3f3f3f,那个 < 就再也刷不动,
一个明明走得到的点会被原样打成「不可达」。
⇒ ★★ 这是本书那条「顶格是顶到题面的边上」的又一个形状:
本章那份代码没有错 —— 本章的题面写着 w ≤ 100,0x3f3f3f3f 绰绰有余。
换了一道题,同一个常量就成了 bug(第 52 章那条
「上一章的正确写法就是这一章的 bug」在解析页上的又一次)。
3★★★ 第一个错法:把本章那份代码原样搬过来
// ✗ 错法①:把本章 naive.cpp **原样搬过来** —— INF 写成 0x3f3f3f3f//// ★★★ 这一份是这一页的主角,因为它**看不出哪里错了**:// 算法一个字没改、输出格式也照题面改成了 2^31−1,// 唯一留着的是本章那个习惯写法 `const int INF = 0x3f3f3f3f;`。//// 而这道题的题面写着 **Σw < 2^31** ——// ⇒ 一条最短路可以长到 **2 147 483 647**;// ⇒ 而 0x3f3f3f3f 只有 **1 061 109 567**(两者差 2.02 倍)。// ⇒ 距离一旦越过这条线,`dist[u] + w < dist[v]` 那个 `<` 就再也刷不动 dist[v],// 于是一个**明明走得到**的点,被原样当成「不可达」打出 2^31−1。//// ⇒ ★★ **「顶格」是顶到题面的边上,不是顶到你顺手写的那个常量上。**// 本章那份代码没错 —— 本章的题面写着 `1 ≤ w ≤ 100`,0x3f3f3f3f 绰绰有余。// **换了一道题,同一个常量就成了 bug**(第 52 章「上一章的正确写法就是这一章的 bug」的又一次)。//// ⚠ 它在顺手随机的小数据上是**精确的 0**:权值小,距离根本够不着 1.06×10⁹。// 要抓它必须专门造一档「权值大到贴着 Σw < 2^31」的数据(生成器档位 1)。
#include <bits/stdc++.h>using namespace std;
const int INF = 0x3f3f3f3f; // ★ 本章那个习惯写法,一个字没改
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
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), vis(n + 1, 0); dist[s] = 0; for (int step = 0; step < n; step++) { int u = -1; for (int j = 1; j <= n; j++) if (!vis[j] && (u == -1 || dist[j] < dist[u])) u = j; if (u == -1 || dist[u] == INF) break; vis[u] = 1; for (auto [v, w] : g[u]) if (dist[u] + w < dist[v]) dist[v] = dist[u] + w; }
for (int i = 1; i <= n; i++) cout << (dist[i] >= INF ? 2147483647LL : (long long)dist[i]) << " \n"[i == n]; return 0;}点「运行 ▶」看结果
这一版只留了本章那一个常量,输出格式已经照题面改成 2³¹ − 1 了。
| 300 轮 | 顺手随机(权值 1~20) | ★ 大权值档 |
|---|---|---|
| 「INF = 0x3f3f3f3f」被抓 | ★ 精确的 0 | ★ 300 / 300 |
大权值档就是「一条几步的链,每步 4.5×10⁸ ~ 5.3×10⁸」:
实测这 300 轮里最远的距离是 2 060 551 036,而全图的 Σw 最大只有 2 060 551 372
—— ★ 一个字都没有违反题面(Σw < 2³¹ = 2 147 483 648)。
⇒ 那个 0 不是「概率低」,是结构上不可能:权值 120、点数 49,
距离连 200 都到不了,离 1.06×10⁹ 差七个数量级。
⚠ 加多少轮都没用,只能改档位(第 5 章 P1042 那条的又一次)。
4⚠ 第二个错法:题面亲手挖的那个坑 —— INF 就写 2³¹ − 1
// ✗ 错法②:INF 就用题面给的那个 2^31−1(= INT_MAX),而且没有那句提前收工的 break//// ★ 这个坑是题面**亲手挖的**:输出格式写着「不能到达则输出 2^31−1」,// 顺手就会想「那我把 INF 直接设成 2^31−1,最后一行连判断都不用写了」。//// 于是当所有还没定下来的点都是 INF 时(= 剩下的全都走不到),// 外层那句「挑最小的」仍然会挑出一个 INF 点,接着去松弛它的邻居://// dist[u] + w = 2147483647 + w ← ★ int 溢出,绕回一个**负数**//// 负数当然比什么都小,于是一堆走不到的点被刷上了负距离。//// ⚠ 演示用的代码不能是未定义行为(不然换个编译器就换一种表现,第 45 章那条),// 所以下面那一行用 unsigned 显式绕回 —— **错法一样,但可复现**。//// ⚠ 触发条件是**图里有走不到的点**(否则那句 break 有没有都一样)。// ⇒ 生成器一旦「顺手保证连通」,它就是精确的 0。
#include <bits/stdc++.h>using namespace std;
const int INF = 2147483647; // ★ 题面那个哨兵值,直接拿来当 INF
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
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), vis(n + 1, 0); dist[s] = 0; for (int step = 0; step < n; step++) { int u = -1; for (int j = 1; j <= n; j++) if (!vis[j] && (u == -1 || dist[j] < dist[u])) u = j; if (u == -1) break; // ⚠ 少了 `|| dist[u] == INF` vis[u] = 1; for (auto [v, w] : g[u]) { int nd = (int)((unsigned)dist[u] + (unsigned)w); // ★ 显式绕回,让这个错可复现 if (nd < dist[v]) dist[v] = nd; } }
for (int i = 1; i <= n; i++) cout << dist[i] << " \n"[i == n]; // INF 恰好就是要输出的 2^31−1 return 0;}点「运行 ▶」看结果
题面写着「不能到达则输出 2³¹ − 1」,顺手就会想:那我把 INF 直接设成 2³¹ − 1, 最后一行连判断都省了。 这一步其实没错 —— 错的是它配套省掉的那半句:
if (u == -1 || dist[u] == INF) break; <- 正解
if (u == -1) break; <- 这一版,少了后半句
剩下的点全是 INF 时,外层照样会挑出一个 INF 点去松弛它的邻居,
于是 2147483647 + w 溢出,绕回一个负数 —— 负数比什么都小,
一堆走不到的点就此被刷上了负距离。
| 300 轮 | 顺手随机 | ★ 「保证 s 走得到所有点」那一档 |
|---|---|---|
| 这一轮里有走不到的点 | 217 | 0 |
| 「INF = INT_MAX」被抓 | 207 | ★ 精确的 0 |
上一步那个 0 是权值太小(改档位才能救),这一步这个 0 是生成器顺手保证了连通。
⇒ 而「顺手保证连通」恰恰是最容易写出来的那种生成器 (「有一串 2³¹−1 的数据看着不像话」)—— 本章第 12 步那张表 早就为这件事记过一笔:那一句在本章值 51 / 300。
★ 顺带一层:有走不到的点 217 轮,而真被抓只有 207 轮 —— 差的那 10 轮里,溢出出来的那个负数恰好没影响任何一个走得到的点。 ⇒ 又一次「满足触发条件 ≠ 一定被抓」(本书量到的比值从 1.0 到 150 倍都有,这次是 1.05)。
5★ 第三个错法:走不到就输出 -1 —— 而它被抓的轮数是一个恒等式
// ✗ 错法③:走不到的点输出 -1 —— 本章题面的格式,照抄过来了//// 算法完全正确,只有最后一行的哨兵值不对。// ★ 这是本书反复说的那一类:**和算法无关,但会挂人的那一条**。// 对拍抓得到它(因为参照物按题面输出 2^31−1),但它提醒的是另一件事:// **换一道题就要重读一遍输出格式**,哪怕算法一个字都不用改。//// ⚠ 被抓的轮数 ≡ 「这一轮里有走不到的点」的轮数 —— 一个不差(度量程序里数了)。
#include <bits/stdc++.h>using namespace std;typedef long long ll;
const ll INF = (ll)4e18;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
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<ll> dist(n + 1, INF); vector<int> vis(n + 1, 0); dist[s] = 0; for (int step = 0; step < n; step++) { int u = -1; for (int j = 1; j <= n; j++) if (!vis[j] && (u == -1 || dist[j] < dist[u])) u = j; if (u == -1 || dist[u] == INF) break; vis[u] = 1; for (auto [v, w] : g[u]) if (dist[u] + w < dist[v]) dist[v] = dist[u] + w; } for (int i = 1; i <= n; i++) cout << (dist[i] == INF ? -1 : dist[i]) << " \n"[i == n]; // ⚠ 本章的格式 return 0;}点「运行 ▶」看结果
算法一个字没错,只有最后一行的哨兵值是本章的格式。
| 300 轮 | |
|---|---|
| 这一轮里有走不到的点 | 217 |
| 「输出 -1」被抓 | ★ 217(一个不差) |
⇒ ★ 这个恒等式本身是白送的(有不可达点 ⟺ 打出哨兵值 ⟺ 两边不同), 它的用处是当自检:这一档要是数出 216,就说明我的度量程序算错了。
⚠ 而它真正提醒的是另一件事:换一道题就要重读一遍输出格式,哪怕算法一个字不用改。
6⚠ 第四个错法:邻接矩阵,重边直接覆盖 —— 而那句 Update 的分量取决于你的存法
// ✗ 错法④:用邻接矩阵存图,重边**直接覆盖**(没取 min)//// 题面 2022/07/29 那句 Update 就是冲着它来的:「两个点之间可能有多条边,敬请注意。」//// ★ 而这句话的分量,**取决于你用哪种存法**(本书第五次量到这件事):// · 邻接表(p3371.cpp):重边原样躺着,松弛时那个 `<` 自己会挑短的 ⇒ **这句话是噪声**;// · 邻接矩阵 + `g[u][v] = w`:后读进来的那条把前面的**盖掉了** ⇒ **这句话是命门**。// (正确的矩阵写法是 `g[u][v] = min(g[u][v], w)`。)//// ⚠ 触发条件是**两层**的:先得有重边,重边里还得是「后来的那条更长」,// 而且被盖掉的那条还得真在某条最短路上。度量程序把这两层分开数了。//// ⚠⚠ 另外这一版**顶格根本开不下**:n = 10⁴ ⇒ 10⁴ × 10⁴ 个 int = **400 MB**,题面只给 128 MB。// ⇒ 所以这道题「朴素 O(n²) 能过」和「可以用邻接矩阵」是两句话,// 前者成立不代表后者成立。这里把 n 限死在 2000 以内,只为了能在对拍里跑起来。
#include <bits/stdc++.h>using namespace std;typedef long long ll;
const ll INF = (ll)4e18;const int MAXN = 2005;static ll g[MAXN][MAXN];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n, m, s; if (!(cin >> n >> m >> s)) return 0; if (n >= MAXN) { cout << "太大了,矩阵开不下\n"; return 0; } for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) g[i][j] = INF; for (int i = 0; i < m; i++) { int u, v, w; cin >> u >> v >> w; g[u][v] = w; // ⚠ 直接覆盖 —— 重边里后来的那条赢了 }
vector<ll> dist(n + 1, INF); vector<int> vis(n + 1, 0); dist[s] = 0; for (int step = 0; step < n; step++) { int u = -1; for (int j = 1; j <= n; j++) if (!vis[j] && (u == -1 || dist[j] < dist[u])) u = j; if (u == -1 || dist[u] == INF) break; vis[u] = 1; for (int v = 1; v <= n; v++) if (g[u][v] != INF && dist[u] + g[u][v] < dist[v]) dist[v] = dist[u] + g[u][v]; } for (int i = 1; i <= n; i++) cout << (dist[i] == INF ? 2147483647LL : dist[i]) << " \n"[i == n]; return 0;}点「运行 ▶」看结果
题面 2022/07/29 那句 Update(「两个点之间可能有多条边」)就是冲着它来的。
| 存法 | 那句 Update 是什么 | 为什么 |
|---|---|---|
| 邻接表(正解) | ★ 噪声 | 重边原样躺在 g[u] 里,松弛时那个 < 自己会挑短的 |
| 邻接矩阵 + 直接赋值 | ★ 命门 | 后读进来的那条把前面的盖掉了(正确写法是 min) |
⇒ 这是本书那条「这句约束重不重要,是「题目 × 你写的那一版」的属性」 在第 32 章的又一次现场(连着第五页成立)。
而它的触发条件是三层的:
| 300 轮(重边密集档:点少边多) | |
|---|---|
| ① 有重边 | 300 |
| ② 而且「后来那条更长」 | 288 |
| ③ 真被抓 | 89 |
第 ③ 层还要求那条被盖掉的短边真在某条最短路上。 ⚠ 顺手随机那一档:有重边 236 轮、真被抓 58 轮; 而「不造重边」那一档是 ★ 精确的 0 —— 又一个结构性的 0。
n = 10⁴ 的 int 矩阵是 10⁴ × 10⁴ × 4 字节 = 400 MB,而题面只给 128 MB。
⇒ ★ 「朴素 O(n²) 能过」和「可以用邻接矩阵」是两句话 —— 这道题前者成立、后者不成立。(本页那份矩阵代码把 n 限死在 2000 以内,只为了能在对拍里跑起来。)
7★ 那「朴素到底能不能过」—— 顶格真跑一遍
本机实测(A 机 · WSL2 · 6.18.33.2-microsoft-standard-WSL2 · 8 线程 / 7 GB ·
2026-08-30 · 独占)。数据来自本页的生成器:./p3371Gen 1 5 10000 500000,
顶格 n = 10⁴、m = 5 × 10⁵:
| 顶格一次 | 时限 | |
|---|---|---|
朴素 O(n² + m)(本页正解) |
0.13 秒 | 1 秒 |
堆优化 O(m log n) |
0.05 秒 | 1 秒 |
⇒ 题单那句「朴素 O(n²) 就能过」是真的,而且余量 7.7 倍。
n² = 10⁸ 这个数在 1 秒的时限下正好是能过的那一档 —— 这也是这道题被叫作「弱化版」的原因。
顶格那份输入是 6 349 003 字节(= 1.5 × 10⁶ 个整数)。同一份数据,只读不算:
| 读法 | 耗时 |
|---|---|
默认 cin |
0.16 秒 |
cin + 关同步 |
★ 0.03 秒 |
scanf |
0.04 秒 |
⇒ 四种读法的倍数跨题几乎不变,变的是绝对时间,而分数线在绝对时间上
(第 6 章 P2367 是 2 × 10⁷ 个数 / 1 秒 ⇒ 连 scanf 都不够;
这道题 1.5 × 10⁶ 个数 / 1 秒 ⇒ 关同步就够,而不关的话光读入就吃掉 16% 的时限)。
8★ 对拍这一页
参照物是 Bellman–Ford(反复松弛所有边,O(nm))——
它没有「取当前最近的那个」这一步,和正解一行代码都不共享。
| 300 轮 | 顺手随机 | 换一档 |
|---|---|---|
| 正解 ≡ Bellman–Ford | ★ 不一致 0 轮 | 0 |
| INF = 0x3f3f3f3f | ★ 精确的 0 | 大权值档 ★ 300 |
| INF = INT_MAX 且不提前收工 | 207 | 保证连通档 ★ 0 |
| 走不到输出 -1 | 217(≡ 有不可达点的轮数) | 保证连通档 ★ 0 |
| 邻接矩阵直接覆盖 | 58 | 不造重边档 ★ 0 / 重边密集档 89 |
这道题的官方样例是一张 4 个点的小图 —— 没有走不到的点、没有重边、权值只有个位数。
⇒ 上面四个错法,它一个都问不出来(四个版本在样例上打的都是 0 2 4 3)。
★ 本书连着量了十几页「官方样例是个「一测就死」的过滤器」 (挡住每组都错的、放过偶尔才错的)—— 这一页是它的极端情形: 四个错法全是「碰上特定形状才错」,于是过滤器一个都没筛下来。
9度量程序和生成器
10一页纸
| ★ 算法 | 就是本章那三句话,一个字不用改 —— 四个错法没有一个错在 Dijkstra 上 |
★★★ INF = 0x3f3f3f3f |
题面 Σw < 2³¹ 允许距离到 2 147 483 647,而它只有 1 061 109 567(差 2.02 倍);顺手随机精确的 0、大权值档 300/300 |
⚠ INF = INT_MAX |
题面那个哨兵值亲手挖的坑:少了 dist[u] == INF 那半句就溢出成负数;保证连通那档是精确的 0 |
| ★ 输出 -1 | 被抓 217 ≡ 有走不到的点的轮数(一个不差)—— 换题就要重读输出格式 |
| ★★ 重边那句 Update | 对邻接表是噪声、对矩阵是命门;触发条件三层:300 → 288 → 89 |
| ⚠ 顶格算术 | 朴素 n² = 10⁸ 0.13 秒能过;但 10⁴ × 10⁴ 的矩阵要 400 MB(只给 128) |
| ⚠ 官方样例 | 四个错法一个都没挡住 —— 那张 4 点小图没有不可达点、没有重边、权值又小 |