0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P4779,日期见页头。两边不一致时信原站。
题目背景
2018 年 7 月 19 日,某位同学在 NOI Day1 T1 归程 一题里非常熟练地使用了一个广为人知的算法求最短路。
然后呢?
100 → 60;
Ag → Cu;
最终,他因此没能与理想的大学达成契约。
小 F 衷心祝愿大家不再重蹈覆辙。
题目描述
给定一个 n 个点,m 条有向边的带非负权图,请你计算从 s 出发,到每个点的距离。
数据保证你能从 s 出发到任意点。
输入格式
第一行为三个正整数 n, m, s。
第二行起 m 行,每行三个非负整数 uᵢ, vᵢ, wᵢ,
表示从 uᵢ 到 vᵢ 有一条权值为 wᵢ 的有向边。
输出格式
输出一行 n 个空格分隔的非负整数,表示 s 到每个点的距离。
说明/提示
样例解释请参考数据随机的模板题。
1 ≤ n ≤ 10⁵;1 ≤ m ≤ 2 × 10⁵;s = 1;1 ≤ uᵢ, vᵢ ≤ n;
0 ≤ wᵢ ≤ 10⁹,0 ≤ Σwᵢ ≤ 10⁹。
本题数据可能会持续更新,但不会重测,望周知。(2018.09.04 数据更新 from @zzq)
时限 1 秒,内存 512 MB。
输入输出样例
1★ 先把两道题的题面并排放一遍
这道题和 P3371 的题面几乎一字不差。逐行比过去,只有五处不同 —— 而每一处都改变了一句结论:
| P3371 | P4779 | 这一处改了什么 | |
|---|---|---|---|
n |
≤ 10⁴ | ★ ≤ 10⁵ | 朴素 O(n²) 从 10⁸ 变成 10¹⁰ |
m |
≤ 5 × 10⁵ | ≤ 2 × 10⁵ | —— |
Σw |
< 2³¹ | ★ ≤ 10⁹ | ★★★ 把「INF 该写多大」整个翻了面 |
s |
任意 | 固定是 1 | 「起点不一定是 1」那个坑在这道题上是噪声 |
| 可达性 | 可能走不到 | ★ 保证都走得到 | 哨兵值那一整支没有了 |
| 数据 | 「保证数据随机」 | ★ 这句话撤了 | ⇒ 题目背景那段故事的全部来处 |
本书反复在说「题面上那几行数字,每一行都是一件工具」。 这两道题把这句话演到了极致:算法一个字都不用改,而五行数据范围里有四行各自否掉了一种写法。
⇒ 所以下面四步分别对着四行:
① n 那行否掉朴素(第 ② 步);② Σw 那行救活了 0x3f3f3f3f(第 ③ 步);
③ 「保证都走得到」那行让哨兵值消失;④ 「保证数据随机」被撤掉 ⇒ SPFA 会被卡(第 ④ 步)。
// P4779【模板】单源最短路径(标准版)—— ★ 这一版就能 AC//// ★ 这道题和 [P3371] 的题面**几乎一字不差**,变的只有数据范围那几行://// P3371 P4779// n ≤ 10⁴ ≤ 10⁵ ← ★ 朴素 O(n²) 从 10⁸ 变成 10¹⁰// m ≤ 5×10⁵ ≤ 2×10⁵// Σw < 2³¹ ≤ 10⁹ ← ★ 这一行把「INF 该写多大」整个翻了面// s 任意 固定是 1// 可达 可能走不到 **保证都走得到**//// ⇒ 于是「哪一版该交」在这两道题上给出**相反**的答案:// P3371 朴素 0.13 秒随便过;这道题朴素 10¹⁰ 次,本机 22.3 秒 —— 只有堆优化能过。// (第 22 章 B3637 / P1020 那条「哪一版该交由题面的 n 说了算」的又一次现场。)//// ★ 而最值钱的一条对照在 INF 上:// 题面 `0 ≤ Σw ≤ 10⁹` ⇒ 任何一条最短路都 ≤ 10⁹ < 0x3f3f3f3f = 1 061 109 567,// ⇒ **同一个 `INF = 0x3f3f3f3f`,在 P3371 上必错、在这道题上恰好安全 —— 余量只有 6.1%。**// 下面仍然用 long long,理由是「不必去记那 6.1%」;p4779Inf3f.cpp 把那一版也留着,// 实测 300 轮和它逐字节相同。
#include <bits/stdc++.h>using namespace std;typedef long long ll;typedef pair<ll, int> PLI;
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); priority_queue<PLI, vector<PLI>, greater<PLI>> 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 i = 1; i <= n; i++) cout << dist[i] << " \n"[i == n]; // 题面保证都走得到,不用管哨兵值 return 0;}点「运行 ▶」看结果
2⚠ 第一件事:同一份朴素代码,从「随便过」变成「必挂」
// ⚠ 朴素 O(n²) Dijkstra —— **答案永远是对的,可它在这道题上跑不完**//// 一个字都没改地从 [P3371] 那一页搬过来(那儿它 0.13 秒随便过)。// 这道题 n ≤ 10⁵ ⇒ 外层挑 n 次、每次扫 n 个点 = **10¹⁰** 次比较。//// 本机实测(顶格 n = 10⁵):**22.3 秒**,时限 1 秒。//// ⇒ ★★ 这是本书那条「[官方样例和对拍都筛不出「答案对但跑不完」](/sol/p5019/)」的又一次:// 它和正解**逐字节相同**,对拍 300 轮 0 不一致 —— 唯一能发现它的办法是// **数一数那个 n² 是多少**,或者真去跑一次顶格。
#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++) // ★ 就是这一行,n 次 × n 个点 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] << " \n"[i == n]; return 0;}点「运行 ▶」看结果
本机实测(A 机 · WSL2 · 6.18.33.2-microsoft-standard-WSL2 · 8 线程 / 7 GB ·
2026-08-30 · 独占)。数据来自本页生成器:./p4779Gen 1 3 100000 200000,
顶格 n = 10⁵、m = 2 × 10⁵(3 282 801 字节):
| 顶格一次 | 时限 | |
|---|---|---|
| 堆优化(正解) | 0.04 秒 | 1 秒 |
朴素 O(n²) |
22.3 秒 | 1 秒 |
⇒ 差 557 倍;而在 P3371 上,同一份朴素代码是 0.13 秒。
朴素版和正解在顶格那组数据上逐字节相同,300 轮对拍也是 0 不一致 —— 它没有错,它只是跑不完。
⇒ 又一次第 20 章 P5019 那条:那个「一测就死」的过滤器筛的是「答案错」,
对「答案对但跑不完」完全无能为力,对拍也一样。
唯一能发现它的办法是把 n² 乘出来(10¹⁰),或者真去跑一次顶格。
3★★★ 第二件事:同一个 0x3f3f3f3f,隔壁那道题上必错,这道题上一分不扣
// ★ 「INF 写成 0x3f3f3f3f、dist 用 int」—— 在这道题上它是**对的**//// 同一个写法在 [P3371] 那一页是本页主角级的 bug(那道题 Σw < 2³¹,距离能到 2 147 483 647)。// 这道题的题面写着 **0 ≤ Σw ≤ 10⁹**://// 任何一条最短路的长度 ≤ Σw ≤ 1 000 000 000 < 0x3f3f3f3f = 1 061 109 567//// ⇒ 它**恰好够**,而余量只有 **6.1%**。// (这也是「[答案 ≥ 任何一个被用到的中间值](/sol/p1164/)」那条论证模式的第四次登场:// 被松弛出来的每一个值都是某条路的长度,所以它们全在 Σw 以内。)//// ★ 这一页留着它,是为了把那句结论钉死:// **「INF 该写多大」不是一个能背的常量,是一道要拿题面乘一遍的算术题** ——// 同一个 0x3f3f3f3f,隔壁那道题上必错,这道题上一分不扣。
#include <bits/stdc++.h>using namespace std;
const int INF = 0x3f3f3f3f;typedef pair<int, int> PII;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr);
int n, m, s; if (!(cin >> n >> m >> s)) return 0; vector<vector<PII>> 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); priority_queue<PII, vector<PII>, greater<PII>> 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 i = 1; i <= n; i++) cout << dist[i] << " \n"[i == n]; return 0;}点「运行 ▶」看结果
P3371 那一页的主角就是这个写法:那道题 Σw < 2³¹,
距离能长到 2 147 483 647,而 0x3f3f3f3f 只有 1 061 109 567 ⇒ 专门造一档就 300 / 300 全错。
这道题只把那一行换成 0 ≤ Σw ≤ 10⁹,两行就能证明它安全:
每一个被松弛出来的值都是某一条路的长度 ⇒ 它 ≤ Σw ≤ 10⁹ < 0x3f3f3f3f = 1 061 109 567。
| P3371 | P4779 | |
|---|---|---|
| 题面允许的最大距离 | 2 147 483 647 | 1 000 000 000 |
0x3f3f3f3f |
1 061 109 567 | 1 061 109 567 |
| 结论 | ★ 不够(差 2.02 倍) | ★ 恰好够,余量 6.1% |
| 专门造的大权值档 300 轮 | ★ 300 / 300 全错 | ★ 0 次错 |
⇒ 这是「答案 ≥ 任何一个被用到的中间值」那条证明模式的第四次登场 (前三次是 P1164、P5365、P1122)—— 它一次又一次地把「要不要 long long / INF 该多大」变成两行推理。
★ 但余量只有 6.1% —— 这也是本页正解仍然用 long long 的理由:
不必去记那 6.1%。
4★★★ 第三件事:题目背景那段故事 —— SPFA 会被卡
原题的题目背景不是段子,是这道题存在的理由: 「某位同学非常熟练地使用了一个广为人知的算法求最短路。然后呢?100 → 60;Ag → Cu。」
// ⚠ SPFA(队列优化的 Bellman–Ford)—— ★ 这道题的**题目背景**就是冲着它写的//// 原题背景原文:「2018 年 7 月 19 日,某位同学在 NOI Day1 T1 里非常熟练地使用了// 一个广为人知的算法求最短路。然后呢?100 → 60;Ag → Cu。」//// SPFA 的答案**永远是对的**(它就是 Bellman–Ford,只是不去松弛那些没变过的点)。// 它的问题只在**次数**上:一个点可以被反复入队,最坏是 O(nm)。//// ★ 而「最坏」不是随机数据能撞出来的(本书第三次撞见这件事,// 前两次是 [P3916] 的 0.23 秒 vs 36.35 秒、[P1141] 的 683 格 vs 10⁶ 格)——// 要**造对形状**:网格图(横边很短、竖边很长)能让 SPFA 反复回头改。// p4779Grid.cpp 就是那个形状,正文第 ⑤ 步把倍数列出来了。//// 这一份用 `deque` 的朴素写法(不做 SLF/LLL 优化),因为要演示的正是它的最坏情形。
#include <bits/stdc++.h>using namespace std;typedef long long ll;
const ll INF = (ll)4e18;
int main(int argc, char** argv) { ios::sync_with_stdio(false); cin.tie(nullptr); bool countMode = (argc > 1 && string(argv[1]) == "count"); // 只打「出队了多少次」
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<char> inq(n + 1, 0); deque<int> q; dist[s] = 0; q.push_back(s); inq[s] = 1; ll pops = 0; while (!q.empty()) { int u = q.front(); q.pop_front(); inq[u] = 0; pops++; // ★ 这就是那把机器无关的尺子 for (auto [v, w] : g[u]) if (dist[u] + w < dist[v]) { dist[v] = dist[u] + w; if (!inq[v]) { inq[v] = 1; q.push_back(v); } } }
if (countMode) { printf("出队 %lld 次\n", pops); return 0; } for (int i = 1; i <= n; i++) cout << dist[i] << " \n"[i == n]; return 0;}点「运行 ▶」看结果
SPFA 就是 Bellman–Ford,只不过只去松弛「刚变过的点」。它的答案永远是对的 —— 问题只在次数上:一个点可以被反复入队。
数据全部合乎题面(无向边拆成两条有向边,s = 1,全图连通,Σw 约 5 × 10⁷)。
尺子是机器无关的那两把:SPFA 出队多少次、Dijkstra 入堆多少次。
| 网格形状 | n |
m |
SPFA 出队 | Dijkstra 入堆 | 倍数 |
|---|---|---|---|---|---|
| 25 × 2000 | 50 000 | 195 950 | 168 291 | 89 903 | 1.9 |
| 100 × 500 | 50 000 | 198 800 | 4 348 978 | 96 160 | 45.2 |
| 1000 × 50 | 50 000 | 197 900 | 84 910 056 | 97 123 | 874.3 |
| ★ 5000 × 10 | 50 000 | 189 980 | ★ 193 543 606 | 94 442 | ★ 2049.3 |
⇒ ★★★ 规模一模一样,形状换一下,SPFA 的工作量涨了 1150 倍 (168 291 → 193 543 606),而 Dijkstra 从头到尾只在 9 万上下动了 5%。
本机实测(同上机器 / 日期)在最后那个形状上:
| 一次 | 时限 | |
|---|---|---|
| SPFA | ★ 3.06 秒 | 1 秒 |
| 堆优化 Dijkstra | 0.02 秒 | 1 秒 |
★ 而两版的输出逐字节相同 —— 又一次「答案对但跑不完」。
为什么这个形状能卡住它:横边很短(13)、竖边很长(11000)。
于是「先沿着一行横着跑很远」这条路会先被算出来;过一会儿某条竖边把上游刷小了,
这一整行又得重算一遍。行越长、行数越多,这种回头就叠得越厉害。
而 Dijkstra 因为「按距离从小到大定死」,一个点只处理一次,根本不会回头。
固定列数 500,只把行数翻倍(n 也跟着翻倍):
| 网格 | n |
SPFA 出队 | 比上一行 |
|---|---|---|---|
| 10 × 500 | 5 000 | 10 454 | —— |
| 20 × 500 | 10 000 | 46 433 | ×4.4 |
| 40 × 500 | 20 000 | 309 474 | ×6.7 |
| 100 × 500 | 50 000 | 4 348 978 | ×14.1 |
n 每翻一倍它涨 4.4 → 6.7 → 14.1 倍,倍数本身在往上走 ——
和第 20 章 P5019 判定那个分治是 O(n²) 用的是同一把尺子。
⇒ ★★ 而这一整节又是「顶格 ≠ 最坏」的第四次现场:
顶格随机那组数据上(n = 10⁵、m = 2 × 10⁵)SPFA 只出队 138 326 次、0.03 秒就完了 —— 大不够,还要形状对。
5★ 对拍这一页:四个版本全都对,它一个也筛不出来
参照物是 Floyd(本章第 4 步那份 brute.cpp)—— 它连「起点」这个概念都没有,
和 Dijkstra 在思路上完全无关。
300 轮(n 随机 4~9,照题面保证全可达、s = 1) |
|
|---|---|
| 正解 ≡ Floyd | ★ 不一致 0 轮 |
朴素 O(n²) |
★ 0(它没错,它是跑不完) |
| INF = 0x3f3f3f3f | ★ 0(这道题它就是对的) |
| SPFA | ★ 0(它没错,它是会被卡) |
这道题上对拍什么也筛不出来 —— 三个「不该交」的版本,没有一个是答案错。
⇒ ★★ 于是这一页的三件事,没有一件是靠对拍发现的:
一件靠乘一遍 n²、一件靠乘一遍 Σw、一件靠造对形状再数次数。
★ 官方样例更是什么都问不出来(四个版本打的都是 0 2 4 3)。
6度量程序和生成器
7一页纸
| ★ 算法 | 和 P3371 一个字不差,变的只有五行数据范围 |
⚠ n ≤ 10⁵ |
朴素 O(n²) = 10¹⁰ ⇒ 22.3 秒(P3371 上同一份是 0.13 秒) |
★★★ Σw ≤ 10⁹ |
救活了 INF = 0x3f3f3f3f(1 061 109 567 > 10⁹,余量 6.1%)—— 而 P3371 上它必错 |
| ★★★ 「数据随机」被撤 | SPFA 会被卡:同样 5 万点 19 万边,只换长宽比就差 1150 倍(3.06 秒 vs 0.02 秒) |
| ★ 对拍 | 四行全是 0 —— 三个「不该交」的版本没有一个是答案错 |
| ★★ 那三件事怎么发现的 | 乘一遍 n² / 乘一遍 Σw / 造对形状再数次数 —— 一件都不靠对拍 |