0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P2865,日期见页头。两边不一致时信原站。
题目描述
Bessie 搬到了一个小农场,有时喜欢回去拜访她的一个好朋友。她不想太快到达她的旧家, 因为她喜欢沿途的风景。她决定选择第二短的路径而不是最短的路径。 她知道一定存在某条第二短路径。
乡村由 R(1 ≤ R ≤ 100000)条双向道路组成,每条道路连接 N(1 ≤ N ≤ 5000)个交叉路口中的两个,
这些交叉路口被方便地编号为 1 到 N。Bessie 从交叉路口 1 出发,她的朋友(目的地)在交叉路口 N。
第二短路径可以与任何最短路径共享道路,并且可以回溯,即多次使用相同的道路或交叉路口。 第二短路径是长度比最短路径长的最短路径 (即,如果存在两条或多条最短路径,第二短路径是长度比这些路径长但不比任何其他路径长的路径)。
输入格式
第 1 行:两个用空格分隔的整数:N 和 R。
第 2 行到第 R+1 行:每行包含三个用空格分隔的整数:A、B 和 D,
描述连接交叉路口 A 和 B 的一条长度为 D(1 ≤ D ≤ 5000)的道路。
输出格式
第 1 行:节点 1 和节点 N 之间第二短路径的长度。
说明/提示
两条路径:1 → 2 → 4(长度 100 + 200 = 300)和 1 → 2 → 3 → 4(长度 100 + 250 + 100 = 450)。
(由 ChatGPT 4o 翻译)
时限 1 秒,内存 512 MB。
输入输出样例
输入
4 4 1 2 100 2 4 200 2 3 250 3 4 100
输出
450
最短路 1 → 2 → 4 是 300,次短 1 → 2 → 3 → 4 是 450。
⚠ 这组样例长得非常「正常」—— 而第 ②③④⑤ 步那四个错法,它一个都没挡住。
1★★★ 先把题面第三段读完:次短路可以回头走
「第二短路径可以与任何最短路径共享道路,并且可以回溯,即多次使用相同的道路或交叉路口。」
把这句话当真,就会看见一个只有两个点、一条边的反例:
2 1
1 2 100最短路是 100;而次短路是 1 → 2 → 1 → 2 = 300 —— 走过去、走回来、再走过去。
⇒ 次短路不一定是一条简单路径。 而「第二短的路径」这几个字最自然的读法恰恰是 「第二短的简单路径」——
// 第一版:把「第二短路径」当成「第二短的**简单路径**」,DFS 枚举//// 这是很自然的第一反应 —— 而题面第三段明写着:// 「第二短路径可以与任何最短路径共享道路,**并且可以回溯,即多次使用相同的道路或交叉路口**。」//// ⚠ 最小反例只要**一条边**:`2 1 / 1 2 100`// 最短路是 100,而次短路是 `1 → 2 → 1 → 2` = **300**(走回去再走回来)。// 枚举简单路径的话,从 1 到 2 只有一条 ⇒ 它根本找不到第二条。// ⚠ 这一版只能跑很小的图(枚举简单路径是指数的),它是用来看清那句题面的,不是拿去交的。
#include <bits/stdc++.h>using namespace std;const int INF = 0x3f3f3f3f;
int n, r;vector<vector<pair<int, int>>> g;vector<char> vis;set<int> lens;
void dfs(int u, int cur) { if (cur > 200000) return; if (u == n) { lens.insert(cur); return; } for (auto [v, w] : g[u]) if (!vis[v]) { vis[v] = 1; dfs(v, cur + w); vis[v] = 0; }}
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); cin >> n >> r; g.assign(n + 1, {}); for (int e = 0; e < r; e++) { int a, b, d; cin >> a >> b >> d; g[a].push_back({b, d}); g[b].push_back({a, d}); } vis.assign(n + 1, 0); vis[1] = 1; dfs(1, 0); if ((int)lens.size() < 2) { cout << INF << "\n"; return 0; } // ✗ 找不到第二条 auto it = lens.begin(); ++it; cout << *it << "\n"; return 0;}点「运行 ▶」看结果
| 300 轮 | 随机图 | ★ 链 | 边权全 1 |
|---|---|---|---|
| 「当成简单路径」被抓 | 169 | ★ 254 | 103 |
★ 「链」那一档最狠(254 / 300),道理和上面那个一条边的反例一样: 链上除了往回蹭,根本没有第二条路可走。
2★ 关键的一步:状态不再只是「在哪个点」
每个点维护两个距离:d1[v](最短)和 d2[v](严格次短)。
一次松弛三选一:
新算出来的 nd |
怎么办 |
|---|---|
nd < d1[v] |
旧的 d1[v] 被挤下来变成 d2[v],nd 当新的 d1[v] |
d1[v] < nd < d2[v] |
换掉 d2[v] |
| 其余 | 丢掉 |
⇒ 复杂度还是 O(R log R),只是堆里的东西翻了一倍。答案就是 d2[N]。
★ 这是第 32 章 P1073 那条「状态里要塞进第二个维度」在这一章的再现 —— 那里的第二维是「贸易做到哪一步」,这里是「第几短」。
// 洛谷 P2865 [USACO06NOV] Roadblocks G —— ★ 这一版就能 AC//// ★ 关键的一步:**状态不再只是「在哪个点」,而是「在哪个点 + 这是到它的第几短」**。// 每个点维护两个距离 d1(最短)和 d2(严格次短),松弛时三选一:// · 比 d1 还短 ⇒ 旧的 d1 被**挤下来**变成 d2,新值当 d1;// · 严格夹在 d1 和 d2 之间 ⇒ 换掉 d2;// · 其余 ⇒ 丢掉。//// ⚠ 「严格」这两个字是题面自己写的:「第二短路径是长度**比最短路径长**的最短路径」。// ⚠ 而次短路**允许重复经过边和点** —— 所以它不一定是一条简单路径(第 ① 步那个反例)。
#include <bits/stdc++.h>using namespace std;const int INF = 0x3f3f3f3f;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, r; cin >> n >> r; vector<vector<pair<int, int>>> g(n + 1); for (int e = 0; e < r; e++) { int a, b, d; cin >> a >> b >> d; g[a].push_back({b, d}); g[b].push_back({a, d}); // 双向道路 }
vector<int> d1(n + 1, INF), d2(n + 1, INF); priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> q; d1[1] = 0; q.push({0, 1}); while (!q.empty()) { auto [d, u] = q.top(); q.pop(); if (d > d2[u]) continue; // ★ 过期的:连次短都不如 for (auto [v, w] : g[u]) { int nd = d + w; if (nd < d1[v]) { d2[v] = d1[v]; // ★ 旧的最短被挤成次短 d1[v] = nd; q.push({d1[v], v}); if (d2[v] < INF) q.push({d2[v], v}); } else if (nd > d1[v] && nd < d2[v]) { // ★ 严格大于 d1 d2[v] = nd; q.push({d2[v], v}); } } } cout << d2[n] << "\n"; return 0;}点「运行 ▶」看结果
3★★★ 第二个错法:把「严格」两个字读丢了
// ✗ 错法①:把「严格次短」写成「非严格次短」//// 题面:「第二短路径是长度**比最短路径长**的最短路径// (即,如果存在两条或多条最短路径,第二短路径是长度比这些路径长但不比任何其他路径长的路径)」// ⇒ 括号里那半句是专门为这个坑写的:**并列的最短路不算第二短。**//// 这一版把 `nd > d1[v]` 写成 `nd >= d1[v]`,于是只要存在两条一样长的最短路,// 它就会把「另一条最短路」当成次短路交上去。
#include <bits/stdc++.h>using namespace std;const int INF = 0x3f3f3f3f;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, r; cin >> n >> r; vector<vector<pair<int, int>>> g(n + 1); for (int e = 0; e < r; e++) { int a, b, d; cin >> a >> b >> d; g[a].push_back({b, d}); g[b].push_back({a, d}); } vector<int> d1(n + 1, INF), d2(n + 1, INF); priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> q; d1[1] = 0; q.push({0, 1}); while (!q.empty()) { auto [d, u] = q.top(); q.pop(); if (d > d2[u]) continue; for (auto [v, w] : g[u]) { int nd = d + w; if (nd < d1[v]) { d2[v] = d1[v]; d1[v] = nd; q.push({d1[v], v}); if (d2[v] < INF) q.push({d2[v], v}); } else if (nd >= d1[v] && nd < d2[v]) { // ✗ 少了一个「严格」 d2[v] = nd; q.push({d2[v], v}); } } } cout << d2[n] << "\n"; return 0;}点「运行 ▶」看结果
题面括号里那半句就是专门为它写的: 「(即,如果存在两条或多条最短路径,第二短路径是长度比这些路径长但不比任何其他路径长的路径)」 ⇒ 并列的最短路不算第二短。
把「图里从 1 到 N 的最短路有没有第二条」(第一层)和「严格 / 非严格两版结论不同」(第二层) 沿边权值域各量 300 张图:
| 边权值域 | 1~8 | 1~2 | ★ 全是 1 |
|---|---|---|---|
| 第一层:存在并列的最短路 | 23 | 64 | 93 |
| 第二层:两版真的算出不同答案 | ★ 23 | ★ 64 | ★ 93 |
★★★ 三个档位一个不差。 而这一次「一个不差」不是巧合,是能写下来的等价:
非严格那版会把「另一条同样长的最短路」收进
d2⇒ 它的答案恒等于最短路长度。 于是两版不同 ⟺ 最短路不止一条。
⇒ 这是本书量过的第五次「触发条件 ≡ 抓获数」(前四次是 P2240、 P1094、P1090、P1077)—— ★ 而报「一个不差」的价值就在于它把结论升级成了判据: 想抓这个 bug,就去造并列的最短路;而造并列最短路的旋钮是边权的值域,不是点数。
| 300 轮真跑对拍 | 随机图(值域 8) | 值域 2 | ★ 边权全 1 |
|---|---|---|---|
| 「严格写成非严格」被抓 | 29 | 70 | ★ 115 |
4⚠ 第三个错法:d1 被刷新时,旧的 d1 没「挤下来」
// ✗ 错法②:d1 被刷新时,忘了把旧的 d1「挤下来」当 d2//// 这一处最容易漏:新来的 nd 比 d1 还短,那**原来那个 d1 就是一条合法的、更长的路** ——// 它应该顺位变成 d2。少了这一行,很多次短路会被直接丢掉。
#include <bits/stdc++.h>using namespace std;const int INF = 0x3f3f3f3f;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, r; cin >> n >> r; vector<vector<pair<int, int>>> g(n + 1); for (int e = 0; e < r; e++) { int a, b, d; cin >> a >> b >> d; g[a].push_back({b, d}); g[b].push_back({a, d}); } vector<int> d1(n + 1, INF), d2(n + 1, INF); priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> q; d1[1] = 0; q.push({0, 1}); while (!q.empty()) { auto [d, u] = q.top(); q.pop(); if (d > d2[u]) continue; for (auto [v, w] : g[u]) { int nd = d + w; if (nd < d1[v]) { d1[v] = nd; // ✗ 旧的 d1 就这么丢了 q.push({d1[v], v}); } else if (nd > d1[v] && nd < d2[v]) { d2[v] = nd; q.push({d2[v], v}); } } } cout << d2[n] << "\n"; return 0;}点「运行 ▶」看结果
新来的 nd 比 d1[v] 还短 ⇒ 原来那个 d1[v] 是一条合法的、更长的走法,它该顺位变成 d2[v]。
少了这一行,很多次短路直接被扔了。
| 300 轮 | 随机图 | 值域 2 | 链 | ★ 边权全 1 |
|---|---|---|---|---|
| 「忘了挤下来」被抓 | 23 | 17 | 3 | ★ 精确的 0 |
★ 边权全 1 那一档的 0 是结构性的:所有边一样长 ⇒ Dijkstra 出队顺序就是层序,
d1[v] 一旦定下来就再没有更短的来刷它 —— 那一行代码根本没机会执行。
⇒ 又一次「为一个 bug 精心造的档位,正是另一个 bug 的盲区」:
同一档把「严格」抬到 115,把这个压到 0。
5⚠ 第四个错法:双向道路只存一遍
// ✗ 错法③:双向道路只存一遍//// 题面:「每条道路连接 N 个交叉路口中的两个」,输入格式那节写的是「描述连接交叉路口 A 和 B 的一条道路」——// 而这道题的次短路**要靠往回走**才走得出来(第 ① 步那个一条边的反例),// ⇒ 少了反向边,连「回溯」这件事都做不到。
#include <bits/stdc++.h>using namespace std;const int INF = 0x3f3f3f3f;
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, r; cin >> n >> r; vector<vector<pair<int, int>>> g(n + 1); for (int e = 0; e < r; e++) { int a, b, d; cin >> a >> b >> d; g[a].push_back({b, d}); // ✗ 少了反着那一行 } vector<int> d1(n + 1, INF), d2(n + 1, INF); priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> q; d1[1] = 0; q.push({0, 1}); while (!q.empty()) { auto [d, u] = q.top(); q.pop(); if (d > d2[u]) continue; for (auto [v, w] : g[u]) { int nd = d + w; if (nd < d1[v]) { d2[v] = d1[v]; d1[v] = nd; q.push({d1[v], v}); if (d2[v] < INF) q.push({d2[v], v}); } else if (nd > d1[v] && nd < d2[v]) { d2[v] = nd; q.push({d2[v], v}); } } } cout << d2[n] << "\n"; return 0;}点「运行 ▶」看结果
这道题的次短路要靠往回走才走得出来(第 ① 步那个一条边的反例)—— 少了反向边,连「回溯」这件事本身都做不到。300 轮抓 227(链那一档 277)。
| 错法 | 300 轮被抓 | 官方样例 |
|---|---|---|
| 当成简单路径 | 169 | ⚠ 放过 |
| 严格写成非严格 | 29 | ⚠ 放过 |
| 忘了把旧 d1 挤下来 | 23 | ⚠ 放过 |
| 双向只存一遍 | 227 | ⚠ 放过 |
⇒ 那组样例是一张四点小图,最短路唯一、不需要回溯、而且 1 → 2 那条边正着就够用 ——
它在结构上问不出这四个问题里的任何一个(P1746 那条「样例挡不住的第三种原因」)。
★ 本轮上一道 B3647 是另一个极端(五个里挡住三个),两头在同一张题单里各出现一次。
6★ 另一条路:两次 Dijkstra + 枚举每条边
// ★ 另一条路:两次 Dijkstra + 枚举每条边//// 从 1 号点跑一次得到 ds[],从 N 号点跑一次得到 dt[](图是无向的,直接反着跑就行)。// 那么「经过边 (u, v)」的最短走法就是 ds[u] + w + dt[v];// 在所有这样的值里挑**严格大于最短路**的最小值,就是次短路。//// ★ 它和 d1/d2 那份一行代码都不共享 —— 对拍时两条独立的路。
#include <bits/stdc++.h>using namespace std;const int INF = 0x3f3f3f3f;
static vector<int> dij(int n, vector<vector<pair<int, int>>>& g, int s) { vector<int> d(n + 1, INF); priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> q; d[s] = 0; q.push({0, s}); while (!q.empty()) { auto [dd, u] = q.top(); q.pop(); if (dd > d[u]) continue; for (auto [v, w] : g[u]) if (dd + w < d[v]) { d[v] = dd + w; q.push({d[v], v}); } } return d;}
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, r; cin >> n >> r; vector<vector<pair<int, int>>> g(n + 1); vector<array<int, 3>> es; for (int e = 0; e < r; e++) { int a, b, d; cin >> a >> b >> d; g[a].push_back({b, d}); g[b].push_back({a, d}); es.push_back({a, b, d}); } auto ds = dij(n, g, 1), dt = dij(n, g, n); int best = ds[n], sec = INF; for (auto& e : es) for (int dir = 0; dir < 2; dir++) { int u = dir ? e[1] : e[0], v = dir ? e[0] : e[1]; if (ds[u] >= INF || dt[v] >= INF) continue; int cand = ds[u] + e[2] + dt[v]; if (cand > best && cand < sec) sec = cand; // ★ 严格大于 } cout << sec << "\n"; return 0;}点「运行 ▶」看结果
从 1 号点跑一次得 ds[],从 N 号点跑一次得 dt[](图无向,直接反着跑),
那么「必须经过边 (u, v)」的最短走法就是 ds[u] + w + dt[v];
在所有这样的值里挑严格大于最短路的最小值。
★ 它和 d1/d2 那份一行代码都不共享,四个档位 1200 轮逐字节相同 ——
第 32 章 P1629 那条「反着跑一次」在无向图上的正面用法。
7★ 对拍这一页
参照物两条路都不走:把所有「(点, 已走长度)」的状态在上界内铺满, 再看到达 N 的长度里第二小的是谁 —— 照「第二短的走法」这句话的字面意思做。
300 轮(n 随机 4~7) |
随机图 | 值域 2 | ★ 链 | ★ 边权全 1 |
|---|---|---|---|---|
d1/d2 ≡ 铺状态 |
★ 0 | ★ 0 | ★ 0 | ★ 0 |
| 枚举边 ≡ 铺状态 | ★ 0 | ★ 0 | ★ 0 | ★ 0 |
| 当成简单路径 | 169 | 117 | ★ 254 | 103 |
| 严格写成非严格 | 29 | 70 | 4 | ★ 115 |
| 忘了把旧 d1 挤下来 | 23 | 17 | 3 | ★ 0 |
| 双向只存一遍 | 227 | 170 | ★ 277 | 135 |
★ 顺带一道三十秒的算术:最短路 ≤ (N−1) × D = 2.5 × 10⁷,次短路再多 2D
⇒ 上界 25 005 000,int 余量 86 倍 ⇒ 不用 long long。
⚠ 而这道题顶格(N = 5000、R = 100000)本机只要 0.02 秒 —— 关卡不在性能上。
8度量程序和生成器
9一页纸
| ★★★ 题面第三段 | 次短路可以回溯 ⇒ 它不一定是简单路径;最小反例只要一条边(100 / 300) |
| ★ 关键的一步 | 状态 = 「在哪个点 + 第几短」⇒ 每个点维护 d1 / d2,松弛三选一 |
| ★★★ 「严格」两个字 | 触发条件 ≡ 抓获数,三个值域一个不差(23 / 64 / 93),而且是能证的等价 |
| ★ 旋钮是值域 | 造并列最短路靠边权值域(8 → 2 → 1:抓 29 → 70 → 115),不是点数 |
| ⚠ 一档只护一半 | 边权全 1 那档把「严格」抬到 115,同时把「忘了挤 d1」压成精确的 0 |
| ⚠⚠ 官方样例 | 四个错法一个都没挡住 —— 「样例是过滤器」那条规律的极端 |
| ★ 另一条路 | 两次 Dijkstra + 枚举每条边,1200 轮逐字节相同 |