0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1629,日期见页头。两边不一致时信原站。
题目描述
有一个邮递员要送东西,邮局在节点 1。他总共要送 n − 1 样东西,
其目的地分别是节点 2 到节点 n。
由于这个城市的交通比较繁忙,因此所有的道路都是单行的,共有 m 条道路。
这个邮递员每次只能带一样东西,并且运送每件物品过后必须返回邮局。
求送完这 n − 1 样东西并且最终回到邮局最少需要的时间。
输入格式
第一行包括两个整数,n 和 m,表示城市的节点数量和道路数量。
第二行到第 (m+1) 行,每行三个整数,u, v, w,表示从 u 到 v 有一条通过时间为 w 的道路。
输出格式
输出仅一行,包含一个整数,为最少需要的时间。
说明/提示
对于 30% 的数据,1 ≤ n ≤ 200。
对于 100% 的数据,1 ≤ n ≤ 10³,1 ≤ m ≤ 10⁵,1 ≤ u, v ≤ n,1 ≤ 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)
~~~~~~~~~~~~~ ~~~~~~~~~~~~~
一次普通的最短路 ★ 这一半才是题目
把每条边 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 邮递员送信 —— ★ 这一版就能 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;}点「运行 ▶」看结果
2⚠⚠ 第一个错法:上一道题的正确写法,在这儿就是 bug
// ✗ 错法②:把图当成无向图存 —— ★ 上一道题 [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;}点「运行 ▶」看结果
g[u].push_back({v, w});
g[v].push_back({u, w}); <- 上一道 P1339 必须写这一行;这道题写了它当场就错
P1339 是无向图,那一行是关键的一步; 这道题题面写着「所有的道路都是单行的」,多存那一遍等于凭空造出一堆反向道路。
⇒ 这是本书那条「上一章的正确写法就是这一章的 bug」的第三次
(前两次是第 52 章的 fa[root] = root、第 27 章 P2016 / P1352 的编号基),
而这一次和上一次一样,两道题就排在同一张题单里、还挨着。
★ 它错的方向不用跑就能判:解的是一个放宽了的问题(路更多了)⇒ 答案恒 ≤ 正解。 实测 300 轮全被抓,而「比正解大」的轮数是 ★ 0(第 26 章那条判据)。
3⚠ 第二个错法:以为「回程 = 去程」,直接乘 2
// ✗ 错法①:以为「回程 = 去程」,把去程那一半直接乘 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;}点「运行 ▶」看结果
| 300 轮 | |
|---|---|
| 被抓 | 299 |
| 其中答案偏大 | 149 |
| 其中答案偏小 | 150 |
- 「当成无向图」:路只多不少 ⇒ 一个方向(恒 ≤ 正解);
- 「回程 = 去程」:它拿去程冒充回程,而单行道上这两个数谁大谁小完全没准 ⇒ 149 轮偏大、150 轮偏小,几乎对半分。
⇒ ★★ 「这个 bug 往哪个方向错」是能判的,但判据是「它解的是哪个问题」, 不是「它看起来像多算还是少算」。
本章第 8 步早就记过一次「两个完全不同的 bug,错得一模一样」
(BFS 版和「堆里放编号」版在那张图上都给 9 10 -1 6 0 2 5)。这一页又撞见一次。
⚠ 而它有多罕见,是量出来的:默认档 300 轮里,两个错法给出同一个数的只有 10 轮。 ⇒ 官方样例正好是那 10 / 300 里的一次。
★ 顺带一句老规矩:对拍看到两份程序答案相同,并不等于它们都对 —— 它们可能只是错在同一个地方。
4⚠ 第三个错法:答案用 int 累加 —— 而这条线一句乘法就能解
// ✗ 错法③:答案用 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;}点「运行 ▶」看结果
这不是一道要靠对拍发现的题,是一道三十秒的算术题:
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)。
第一个撑破 int 的 n |
★ 464 |
顶格 n = 1000 时的答案 |
9 990 000 000 |
是 int 上限的 |
★ 4.65 倍 |
「第一个撑破 int 的 n 是 464」是拿公式算出来的。
度量程序真造了一个 464 个点的环跑一遍:答案 2 148 320 000,
和公式 10⁴ × 464 × 463 一模一样。
⇒ 这是本书那条「报一个算出来的结论之前,先拿真程序验一次」的又一次 —— 公式和代码里的那个环,必须是同一个东西。
★ 顺带:dist 本身(10⁷)在 int 里绰绰有余,撑破的是「把它们加起来」那一步。
⇒ 「要不要 long long」问的从来不是「这道题的数大不大」,而是「哪一个量会大」。
5⚠⚠ 第四个版本:不用反图,每个点各跑一次 —— 我的草稿在这儿被打回来了
想不到反图,最自然的写法就是:要 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 = 1000、m = 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(它连「从哪儿出发」这个概念都没有)。
300 轮(n 随机 4~9,先造一个单向环保证强连通) |
|
|---|---|
| 正解 ≡ Floyd | ★ 不一致 0 轮 |
正解 ≡ n 次 Dijkstra |
★ 不一致 0 轮(它没错,它只是余量小) |
| 当成无向图 | 300(★ 而且恒 ≤ 正解) |
| 回程 = 去程 × 2 | 299(偏大 149、偏小 150) |
| 两个错法给出同一个错数 | 10 |
题面保证「任意两点都能互相到达」(强连通)。
而随手撒一堆有向边,强连通的概率很低 —— 一旦不强连通,
就有一堆 dist = ∞,题面根本没定义那种输入。
⇒ 最省事的办法是先把所有点串成一个单向环(n 条边),再往上撒随机边。
★ 这和上一道 P1339 是同一个动作:
「题面保证了什么」得由生成器亲手做到,否则对拍比的是两个都没定义的东西。
7度量程序和生成器
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 |