题单 · 习题解析

洛谷 P4779 【模板】单源最短路径(标准版)

★★★ 和 P3371 题面一字不差,只有五行数据范围不同 —— 而**每一行各否掉一种写法**:n ≤ 10⁵ 让朴素从 0.13 秒变 **22.3 秒**;★★★ 而 Σw ≤ 10⁹ **救活**了 `INF = 0x3f3f3f3f`(余量只有 **6.1%**,隔壁那道题上它必错);★★★ 「保证数据随机」被撤 ⇒ SPFA 被卡:**同样 5 万点 19 万边,只换长宽比就差 1150 倍**;⚠ 对拍四行全是 0 —— 三件事没有一件靠对拍发现

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

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

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 = 11 ≤ uᵢ, vᵢ ≤ n0 ≤ wᵢ ≤ 10⁹0 ≤ Σwᵢ ≤ 10⁹

本题数据可能会持续更新,但不会重测,望周知。(2018.09.04 数据更新 from @zzq)

时限 1 秒,内存 512 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

P3371同一组样例 —— 原站的样例解释干脆就写着「请参考那道题」。

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.cpp★ 这一版就能 AC(堆优化 O(m log n))
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

2⚠ 第一件事:同一份朴素代码,从「随便过」变成「必挂」

p4779Naive.cpp⚠ 朴素 O(n²) —— 答案永远对,可它跑不完
// ⚠ 朴素 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

本机实测(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 那条:那个「一测就死」的过滤器筛的是「答案错」, 对「答案对但跑不完」完全无能为力,对拍也一样。 唯一能发现它的办法是 乘出来(10¹⁰),或者真去跑一次顶格。

3★★★ 第二件事:同一个 0x3f3f3f3f,隔壁那道题上必错,这道题上一分不扣

p4779Inf3f.cpp★ INF = 0x3f3f3f3f + int —— 在这道题上它是对的
// ★ 「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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

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 次错
★★ 「INF 该写多大」不是一个能背的常量,是一道要拿题面乘一遍的算术题

⇒ 这是「答案 ≥ 任何一个被用到的中间值」那条证明模式的第四次登场 (前三次是 P1164、P5365、P1122)—— 它一次又一次地把「要不要 long long / INF 该多大」变成两行推理。

★ 但余量只有 6.1% —— 这也是本页正解仍然用 long long 的理由: 不必去记那 6.1%。

4★★★ 第三件事:题目背景那段故事 —— SPFA 会被卡

原题的题目背景不是段子,是这道题存在的理由: 「某位同学非常熟练地使用了一个广为人知的算法求最短路。然后呢?100 → 60;Ag → Cu。」

p4779Spfa.cpp⚠ SPFA —— 答案永远对,最坏情况 O(nm)
// ⚠ 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

SPFA 就是 Bellman–Ford,只不过只去松弛「刚变过的点」。它的答案永远是对的 —— 问题只在次数上:一个点可以被反复入队。

★★★ 同样 5 万个点、同样 19 万条边,只换网格的长宽比 —— 差 1150 倍

数据全部合乎题面(无向边拆成两条有向边,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 秒

★ 而两版的输出逐字节相同 —— 又一次「答案对但跑不完」。

p4779Grid.cpp(卡 SPFA 的那个形状)数据生成器

为什么这个形状能卡住它:横边很短(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 在思路上完全无关。

p4779Brute.cpp参照物:Floyd(300 轮不一致 0 轮)
300 轮(n 随机 4~9,照题面保证全可达、s = 1
正解 ≡ Floyd 不一致 0 轮
朴素 O(n²) 0(它没错,它是跑不完)
INF = 0x3f3f3f3f 0(这道题它就是对的)
SPFA 0(它没错,它是会被卡)
⚠⚠ 一张四行全是 0 的对拍表,本身就是这一页的结论

这道题上对拍什么也筛不出来 —— 三个「不该交」的版本,没有一个是答案错

⇒ ★★ 于是这一页的三件事,没有一件是靠对拍发现的: 一件靠乘一遍 、一件靠乘一遍 Σw、一件靠造对形状再数次数。 ★ 官方样例更是什么都问不出来(四个版本打的都是 0 2 4 3)。

6度量程序和生成器

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

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 —— 三个「不该交」的版本没有一个是答案错
★★ 那三件事怎么发现的 乘一遍 / 乘一遍 Σw造对形状再数次数 —— 一件都不靠对拍