题单 · 习题解析

洛谷 P1629 邮递员送信

★ 回程 = **反图上**从 1 号跑一次,一次算完全部 n−1 个回程;★★ 而「当成无向图」这个错法,正是**上一道 P1339 的正确写法** —— 同一张题单、还挨着(恒 ≤ 正解);⚠ 「回程 = 去程 × 2」被抓 299/300 而**两头都跑偏**(偏大 149、偏小 150);⚠ int 溢出一句乘法就解出来(单向环,第一个撑破的 n = **464**);⚠⚠ 草稿被打回 —— 「n 次 Dijkstra 必然 TLE」是错的,本机 **0.43 秒**

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

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

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

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

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

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

题目描述

有一个邮递员要送东西,邮局在节点 1。他总共要送 n − 1 样东西, 其目的地分别是节点 2 到节点 n。 由于这个城市的交通比较繁忙,因此所有的道路都是单行的,共有 m 条道路。 这个邮递员每次只能带一样东西,并且运送每件物品过后必须返回邮局。 求送完这 n − 1 样东西并且最终回到邮局最少需要的时间。

输入格式

第一行包括两个整数,nm,表示城市的节点数量和道路数量。

第二行到第 (m+1) 行,每行三个整数,u, v, w,表示从 uv 有一条通过时间为 w 的道路。

输出格式

输出仅一行,包含一个整数,为最少需要的时间。

说明/提示

对于 30% 的数据,1 ≤ n ≤ 200

对于 100% 的数据,1 ≤ n ≤ 10³1 ≤ m ≤ 10⁵1 ≤ u, v ≤ n1 ≤ w ≤ 10⁴输入保证任意两点都能互相到达

时限 1 秒,内存 128 MB。

输入输出样例

输入

5 10
2 3 5
1 5 5
3 5 6
1 2 8
1 3 8
5 3 4
4 1 8
4 5 3
3 5 6
5 4 2

输出

83

5 个点、10 条单向道路。答案 83 = 去程四段 + 回程四段。 ⚠ 请注意这十行里有重边(3 5 6 出现了两次),也有「只有一个方向」的路。

1★ 关键的一步:把所有边反过来,再跑一次

把题意写成一个式子,这道题就只剩一件事:

答案 = Σ dist(1 → i) + Σ dist(i → 1)        (i = 2 … n)
       ~~~~~~~~~~~~~   ~~~~~~~~~~~~~
       一次普通的最短路   ★ 这一半才是题目
★ 「所有 i 到 1 的最短路」= 反图上「1 到所有 i」的最短路

把每条边 u → v 换成 v → u,得到反图

反图上一条 1 → i 的路,把它逐条边反过来,就是原图上一条 i → 1 的路,长度一模一样; 反过来也一样。⇒ 两边的路一一对应 ⇒ 最短的那条也对应。

⇒ ★ 一次 Dijkstra 就把全部 n−1 个回程算完了,而不是跑 n−1 次。 建反图只要在读入那一行边的时候多写一句:

g[u].push_back({v, w});      // 原图
rg[v].push_back({u, w});     // ★ 反图:同一条边,两头调过来
p1629.cpp★ 这一版就能 AC(两次 Dijkstra)
// P1629 邮递员送信 —— ★ 这一版就能 AC
//
// 题意翻译成两句话:
// 答案 = Σ dist(1 → i) + Σ dist(i → 1) (i = 2 … n)
// 前一半是一次普通的单源最短路;★ 后一半才是这道题的**关键一步**。
//
// ★ 关键的一步:**把所有边反过来,建一张反图,在反图上从 1 号跑一次**。
// 为什么对:反图上「1 → i」的每一条路,把它逐条边反过来,
// 就是原图上一条「i → 1」的路,长度一模一样 ⇒ 两个集合一一对应 ⇒ 最短的那条也对应。
// ⇒ **一次 Dijkstra 就算完了全部 n−1 个「回程」**,而不是跑 n 次。
//
// ⚠⚠ 而这道题和上一道 [P1339] 正好构成一对反面:
// P1339 是无向图,那一行 `g[v].push_back({u, w})` 是**必须写的**;
// 这道题「所有的道路都是单行的」,**写了那一行当场就错**。
// ⇒ 第 52 章那条「上一章的正确写法就是这一章的 bug」,在**同一张题单里**又演了一次
// (上一次是第 27 章 P2016 / P1352 的编号基)。
//
// ⚠ 一道三十秒的算术题:n ≤ 1000、w ≤ 10⁴ ⇒ 单条最短路最长 999 × 10⁴ ≈ 10⁷;
// 要加 2 × 999 个这样的数 ⇒ 答案能到 **2 × 10¹⁰**,而 int 只到 2.1 × 10⁹。
// ⇒ **必须 long long**(p1629Int.cpp 把这条线量出来了)。
//
// 复杂度 O(m log n) × 2,顶格 m = 10⁵ ⇒ 本机 0.01 秒。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<ll, int> PLI;
const ll INF = (ll)4e18;
/* 在给定的邻接表上从 1 号跑一次 Dijkstra,返回 dist[] */
static vector<ll> dij(const vector<vector<pair<int, int>>>& g, int n) {
vector<ll> dist(n + 1, INF);
priority_queue<PLI, vector<PLI>, greater<PLI>> q;
dist[1] = 0;
q.push({0, 1});
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}); }
}
return dist;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
if (!(cin >> n >> m)) return 0;
vector<vector<pair<int, int>>> g(n + 1), rg(n + 1);
for (int i = 0; i < m; i++) {
int u, v, w;
cin >> u >> v >> w;
g[u].push_back({v, w}); // 原图:单向
rg[v].push_back({u, w}); // ★ 反图:同一条边,两头调过来
}
vector<ll> a = dij(g, n), b = dij(rg, n);
ll ans = 0;
for (int i = 2; i <= n; i++) ans += a[i] + b[i];
cout << ans << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

2⚠⚠ 第一个错法:上一道题的正确写法,在这儿就是 bug

p1629Undir.cpp✗ 把图当成无向图存(样例打 56,答案是 83)
// ✗ 错法②:把图当成无向图存 —— ★ 上一道题 [P1339] 的**正确写法**,在这儿就是 bug
//
// g[u].push_back({v, w});
// g[v].push_back({u, w}); <- P1339 必须写这一行;这道题写了它当场就错
//
// 题面:「由于这个城市的交通比较繁忙,因此**所有的道路都是单行的**」。
// 多存那一遍等于凭空造出一堆反向道路 ⇒ 路只会更短 ⇒ ★ **答案恒 ≤ 正解**
// ([第 26 章那条判据](/sol/p1220/):解的是一个**放宽了**的问题 ⇒ 恒 ≤ 最优。
// 度量程序里 300 轮一次没反过来。)
//
// ⇒ ★★ 这是本书那条「**上一章的正确写法就是这一章的 bug**」在**同一张题单里**的现场
// (第 52 章 `fa[root] = root`、第 27 章 P2016 / P1352 的编号基之后,第三次)。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<ll, int> PLI;
const ll INF = (ll)4e18;
static vector<ll> dij(const vector<vector<pair<int, int>>>& g, int n) {
vector<ll> dist(n + 1, INF);
priority_queue<PLI, vector<PLI>, greater<PLI>> q;
dist[1] = 0;
q.push({0, 1});
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}); }
}
return dist;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
if (!(cin >> n >> m)) 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});
g[v].push_back({u, w}); // ⚠ 多存的这一遍就是 bug
}
vector<ll> a = dij(g, n);
ll ans = 0;
for (int i = 2; i <= n; i++) ans += a[i] * 2; // 无向图上去程 = 回程,于是也就乘 2
cout << ans << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
g[u].push_back({v, w});
g[v].push_back({u, w});     <- 上一道 P1339 必须写这一行;这道题写了它当场就错
★★ 同一张题单里,上一道的正确写法就是下一道的 bug

P1339无向图,那一行是关键的一步; 这道题题面写着「所有的道路都是单行的」,多存那一遍等于凭空造出一堆反向道路。

⇒ 这是本书那条「上一章的正确写法就是这一章的 bug」的第三次 (前两次是第 52 章的 fa[root] = root第 27 章 P2016 / P1352 的编号基), 而这一次和上一次一样,两道题就排在同一张题单里、还挨着

★ 它错的方向不用跑就能判:解的是一个放宽了的问题(路更多了)⇒ 答案恒 ≤ 正解。 实测 300 轮全被抓,而「比正解大」的轮数是 ★ 0第 26 章那条判据)。

3⚠ 第二个错法:以为「回程 = 去程」,直接乘 2

p1629Twice.cpp✗ 去程算完 × 2(样例也打 56)
// ✗ 错法①:以为「回程 = 去程」,把去程那一半直接乘 2
//
// 这是最顺手的第一反应 —— 而它是**无向图的思维**。
// 题面白纸黑字写着「**所有的道路都是单行的**」:
// 从 1 走到 i 花 5,从 i 走回 1 完全可以花 50,也可以走另一条完全不同的路。
//
// ⚠ 它的方向是可判的:回程的最短路 ≥ 0,而去程 × 2 = 去程 + 去程 ——
// 两者谁大谁小**不一定**(度量程序里两个方向都数了)。
// ⇒ 这一条和 [P1339] 那个「只存一遍边恒 ≥ 正解」不一样:**这个错法两头都会跑偏。**
#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;
if (!(cin >> n >> m)) 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[1] = 0;
q.push({0, 1});
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}); }
}
ll ans = 0;
for (int i = 2; i <= n; i++) ans += dist[i] * 2; // ⚠ 就是这个 × 2
cout << ans << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
300 轮
被抓 299
其中答案偏大 149
其中答案偏小 150
★ 两个错法,一个只往一边错、一个两头都错 —— 而这不是巧合
  • 「当成无向图」:路只多不少 ⇒ 一个方向(恒 ≤ 正解);
  • 「回程 = 去程」:它拿去程冒充回程,而单行道上这两个数谁大谁小完全没准 ⇒ 149 轮偏大、150 轮偏小,几乎对半分。

⇒ ★★ 「这个 bug 往哪个方向错」是能判的,但判据是「它解的是哪个问题」, 不是「它看起来像多算还是少算」。

★ 而这两个 bug 在官方样例上打出了同一个数 —— 56

本章第 8 步早就记过一次「两个完全不同的 bug,错得一模一样」 (BFS 版和「堆里放编号」版在那张图上都给 9 10 -1 6 0 2 5)。这一页又撞见一次。

⚠ 而它有多罕见,是量出来的:默认档 300 轮里,两个错法给出同一个数的只有 10 轮。 ⇒ 官方样例正好是那 10 / 300 里的一次。

★ 顺带一句老规矩:对拍看到两份程序答案相同,并不等于它们都对 —— 它们可能只是错在同一个地方。

4⚠ 第三个错法:答案用 int 累加 —— 而这条线一句乘法就能解

p1629Int.cpp✗ 32 位累加(样例照过,顶格差 4.65 倍)
// ✗ 错法③:答案用 int 累加
//
// 一道三十秒的算术题:
// n ≤ 1000、w ≤ 10⁴ ⇒ 一条最短路最长 999 × 10⁴ ≈ 10⁷;
// 要加 2 × (n−1) = 1998 个这样的数 ⇒ **答案能到 2 × 10¹⁰**,而 int 只到 2 147 483 647。
//
// ⚠ 而这个上界不是纸上谈兵:一个**单向环** 1 → 2 → … → n → 1、每条边都 10⁴,
// 答案正好是 10⁴ × n(n−1) ≈ 10¹⁰ —— 生成器档位 1 造的就是它,
// 实测顶格答案 **9 990 000 000**,是 int 上限的 **4.65 倍**。
//
// ⚠ 演示用的代码不许是未定义行为(第 45 章那条),所以这里用 unsigned 显式绕回。
// ★ 顺带:dist 本身(10⁷)在 int 里绰绰有余 —— **撑破的是「把它们加起来」那一步**。
// ⇒ 「要不要 long long」问的从来不是「这道题的数大不大」,而是「**哪一个量**会大」。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<ll, int> PLI;
const ll INF = (ll)4e18;
static vector<ll> dij(const vector<vector<pair<int, int>>>& g, int n) {
vector<ll> dist(n + 1, INF);
priority_queue<PLI, vector<PLI>, greater<PLI>> q;
dist[1] = 0;
q.push({0, 1});
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}); }
}
return dist;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
if (!(cin >> n >> m)) return 0;
vector<vector<pair<int, int>>> g(n + 1), rg(n + 1);
for (int i = 0; i < m; i++) {
int u, v, w;
cin >> u >> v >> w;
g[u].push_back({v, w});
rg[v].push_back({u, w});
}
vector<ll> a = dij(g, n), b = dij(rg, n);
unsigned ans = 0; // ⚠ 32 位;用 unsigned 让绕回可复现
for (int i = 2; i <= n; i++) ans += (unsigned)a[i] + (unsigned)b[i];
cout << (int)ans << '\n';
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

这不是一道要靠对拍发现的题,是一道三十秒的算术题:

n ≤ 1000w ≤ 10⁴ ⇒ 一条最短路最长 999 × 10⁴ ≈ 10⁷; 要把 2 × (n−1) = 1998 个这样的数加起来 ⇒ 答案能到 2 × 10¹⁰, 而 int 只到 2 147 483 647。

而它是够得着的:造一个单向环 1 → 2 → … → n → 1、每条边都 10⁴, 答案正好是 10⁴ × n(n−1)

第一个撑破 intn 464
顶格 n = 1000 时的答案 9 990 000 000
int 上限的 4.65 倍
⚠ 而那个 464 配了一次自检

「第一个撑破 int 的 n 是 464」是拿公式算出来的。 度量程序真造了一个 464 个点的环跑一遍:答案 2 148 320 000, 和公式 10⁴ × 464 × 463 一模一样

⇒ 这是本书那条「报一个算出来的结论之前,先拿真程序验一次」的又一次 —— 公式和代码里的那个环,必须是同一个东西。

★ 顺带:dist 本身(10⁷)在 int 里绰绰有余,撑破的是「把它们加起来」那一步。 ⇒ 「要不要 long long」问的从来不是「这道题的数大不大」,而是「哪一个量会大」。

5⚠⚠ 第四个版本:不用反图,每个点各跑一次 —— 我的草稿在这儿被打回来了

p1629NDij.cpp⚠ n 次 Dijkstra —— 答案对,而且它其实能过

想不到反图,最自然的写法就是:要 dist(i → 1),那就从 i 出发跑一次,读第 1 格,跑 n−1 次。

草稿里我写的是「它必然超时」 —— 理由听着很顺:n × m = 10⁸,再乘个 log

本机实测(A 机 · WSL2 · 6.18.33.2-microsoft-standard-WSL2 · 8 线程 / 7 GB · 2026-08-30 · 独占;数据 ./p1629Gen 1 2 1000 100000,顶格 n = 1000m = 10⁵):

写法 顶格一次 时限
★ 正解(两次 Dijkstra) 0.01 秒 1 秒
n 次 Dijkstra 0.43 秒 1 秒
Floyd O(n³) 0.63 秒 1 秒
⚠⚠ 三种写法在本机全都过了 —— 而这才是该说的话

n 次 Dijkstra 不是「过不了」,是「余量只有 2.3 倍」;Floyd 更只剩 1.6 倍。 而评测机通常比本机慢 —— 2.3 倍的余量在考场上是拿命赌。

★ 为什么比估的快:m log n 是个非常松的上界,一条边只有真把某个点刷小了才入堆 (本章第 7 步把这笔账摊开过:900 万条边只带来 19 847 次入堆)。 拿机器无关的尺子量,默认档 300 轮合计入堆次数是 4 249(正解)vs 14 829(n 次), 只差 3.5 倍 —— 而不是 n 倍。

⇒ ★★ 又一次「口诀要拿实测复核,别默认它到处成立」。 「反图」值的不是「从 TLE 变 AC」,是「从 2.3 倍余量变成 100 倍余量」。

6★ 对拍这一页

参照物是 Floyd(它连「从哪儿出发」这个概念都没有)。

p1629Brute.cpp参照物:Floyd(300 轮不一致 0 轮)
300 轮(n 随机 4~9,先造一个单向环保证强连通)
正解 ≡ Floyd 不一致 0 轮
正解 ≡ n 次 Dijkstra 不一致 0 轮(它没错,它只是余量小)
当成无向图 300(★ 而且恒 ≤ 正解
回程 = 去程 × 2 299(偏大 149、偏小 150)
两个错法给出同一个错数 10
⚠ 生成器为什么要先造一个单向环

题面保证「任意两点都能互相到达」(强连通)。 而随手撒一堆有向边,强连通的概率很低 —— 一旦不强连通, 就有一堆 dist = ∞,题面根本没定义那种输入。

⇒ 最省事的办法是先把所有点串成一个单向环n 条边),再往上撒随机边。 ★ 这和上一道 P1339 是同一个动作: 「题面保证了什么」得由生成器亲手做到,否则对拍比的是两个都没定义的东西。

7度量程序和生成器

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

8一页纸

★ 关键的一步 回程 = 反图上从 1 号跑一次 ⇒ 一次算完全部 n−1 个回程
★★ 当成无向图 上一道 P1339 的正确写法,在这儿就是 bug(同一张题单、还挨着);恒 ≤ 正解
⚠ 回程 = 去程 × 2 被抓 299 / 300,而两头都跑偏(偏大 149、偏小 150)—— 和上一条正好对照
★ 两个 bug 打出同一个数 官方样例上都是 56,而 300 轮里这种巧合只有 10
int 溢出 一句乘法就够:单向环上答案 = 10⁴ × n(n−1)第一个撑破 int 的 n 是 464,顶格 4.65 倍;⚠ 撑破的是求和那一步,不是 dist 本身
⚠⚠ 草稿被实测打回来 「n 次 Dijkstra 必然 TLE」是错的:本机 0.43 秒(Floyd 0.63 秒)—— 反图值的是余量,不是 AC