题单 · 习题解析

洛谷 P3371 【模板】单源最短路径(弱化版)

★ 算法就是本章那三句话,四个错法**没有一个错在 Dijkstra 上**;★★★ 主角是 `INF = 0x3f3f3f3f` —— 题面 Σw < 2³¹ 允许距离到 2 147 483 647,而它只有 1 061 109 567(**差 2.02 倍**):顺手随机**精确的 0**、大权值档 **300/300**;★★ 「重边」那句 Update 对邻接表是噪声、对矩阵是命门;⚠ 官方样例四个错法**一个都没挡住**

原题:洛谷 P3371出自 第 32 章 最短路一:Dijkstra 的题单题面本地存档:2026-08-30
⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目背景

本题测试数据为随机数据,在考试中可能会出现构造数据让 SPFA 不通过, 如有需要请移步 P4779

题目描述

如题,给出一个有向图,请输出从某一点出发到所有点的最短路径长度。

输入格式

第一行包含三个整数 n, m, s,分别表示点的个数、有向边的个数、出发点的编号。

接下来 m 行每行包含三个整数 u, v, w,表示一条 u → v 的,长度为 w 的边。

输出格式

输出一行 n 个整数,第 i 个表示 s 到第 i 个点的最短路径, 若不能到达则输出 2³¹ − 1

说明/提示

【数据范围】

对于 20% 的数据:1 ≤ n ≤ 51 ≤ m ≤ 15; 对于 40% 的数据:1 ≤ n ≤ 1001 ≤ m ≤ 10⁴; 对于 70% 的数据:1 ≤ n ≤ 10001 ≤ m ≤ 10⁵; 对于 100% 的数据:1 ≤ n ≤ 10⁴1 ≤ m ≤ 5 × 10⁵1 ≤ u, v ≤ nw ≥ 0Σw < 2³¹,保证数据随机。

Update 2022/07/29:两个点之间可能有多条边,敬请注意。

对于真正 100% 的数据,请移步 P4779。 请注意,该题与本题数据范围略有不同。

样例说明:

P3371 样例说明

(原站附注:图片 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.cpp★ 这一版就能 AC(朴素 O(n² + m))
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

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★★★ 第一个错法:把本章那份代码原样搬过来

p3371Inf3f.cpp✗ INF 写成 0x3f3f3f3f(输出格式已经改对了)
// ✗ 错法①:把本章 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

这一版只留了本章那一个常量,输出格式已经照题面改成 2³¹ − 1 了。

★★★ 顺手随机是精确的 0,而专门造一档就是 300 / 300
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

p3371IntMax.cpp✗ INF = INT_MAX,而且少了那句提前收工
// ✗ 错法②: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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

题面写着「不能到达则输出 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 是权值太小(改档位才能救),这一步这个 0 是生成器顺手保证了连通

⇒ 而「顺手保证连通」恰恰是最容易写出来的那种生成器 (「有一串 2³¹−1 的数据看着不像话」)—— 本章第 12 步那张表 早就为这件事记过一笔:那一句在本章值 51 / 300

★ 顺带一层:有走不到的点 217 轮,而真被抓只有 207 轮 —— 差的那 10 轮里,溢出出来的那个负数恰好没影响任何一个走得到的点。 ⇒ 又一次「满足触发条件 ≠ 一定被抓」(本书量到的比值从 1.0 到 150 倍都有,这次是 1.05)。

5★ 第三个错法:走不到就输出 -1 —— 而它被抓的轮数是一个恒等式

p3371Neg1.cpp✗ 照本章的格式输出 -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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

算法一个字没错,只有最后一行的哨兵值是本章的格式。

300 轮
这一轮里有走不到的点 217
「输出 -1」被抓 217(一个不差)

⇒ ★ 这个恒等式本身是白送的(有不可达点 ⟺ 打出哨兵值 ⟺ 两边不同), 它的用处是当自检:这一档要是数出 216,就说明我的度量程序算错了。

⚠ 而它真正提醒的是另一件事:换一道题就要重读一遍输出格式,哪怕算法一个字不用改。

6⚠ 第四个错法:邻接矩阵,重边直接覆盖 —— 而那句 Update 的分量取决于你的存法

p3371Mat.cpp✗ g[u][v] = w,后来的那条盖掉前面的
// ✗ 错法④:用邻接矩阵存图,重边**直接覆盖**(没取 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

题面 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 秒的时限下正好是能过的那一档 —— 这也是这道题被叫作「弱化版」的原因。

p3371Heap.cpp堆优化版(这道题用不着,留给 P4779)
★ 顺带把读入这笔账也算了:6.35 MB

顶格那份输入是 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))—— 它没有「取当前最近的那个」这一步,和正解一行代码都不共享

p3371Brute.cpp参照物:Bellman–Ford(300 轮不一致 0 轮)
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度量程序和生成器

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

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 点小图没有不可达点、没有重边、权值又小