0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1119,日期见页头。两边不一致时信原站。
题目背景
B 地区在地震过后,所有村庄都遭受了一定的损毁,而这场地震却没对公路造成什么影响。 但是在村庄重建好之前,所有与未重建完成的村庄相连的公路均无法通车。 换句话说,只有连接着两个重建完成的村庄的公路才能通车,只能到达重建完成的村庄。
题目描述
给出 B 地区的村庄数 N,村庄编号从 0 到 N−1,和所有 M 条公路的长度,公路是双向的。
并给出第 i 个村庄重建完成的时间 tᵢ,你可以认为是同时开始重建并在第 tᵢ 天重建完成,
并且在当天即可通车。若 tᵢ 为 0 则说明地震未对此地区造成损坏,一开始就可以通车。
之后有 Q 个询问 (x, y, t),对于每个询问你要回答在第 t 天,
从村庄 x 到村庄 y 的最短路径长度为多少。
如果无法找到从 x 村庄到 y 村庄的路径,经过若干个已重建完成的村庄,
或者村庄 x 或村庄 y 在第 t 天仍未重建完成,则需要输出 -1。
输入格式
第一行包含两个正整数 N, M,表示了村庄的数目与公路的数量。
第二行包含 N 个非负整数 t₀, t₁, …, t_{N−1},表示了每个村庄重建完成的时间,
数据保证了 t₀ ≤ t₁ ≤ … ≤ t_{N−1}。
接下来 M 行,每行 3 个非负整数 i, j, w,w 不超过 10000,
表示了有一条连接村庄 i 与村庄 j 的道路,长度为 w,
保证 i ≠ j,且对于任意一对村庄只会存在一条道路。
接下来一行也就是 M+3 行包含一个正整数 Q,表示 Q 个询问。
接下来 Q 行,每行 3 个非负整数 x, y, t,询问在第 t 天,
从村庄 x 到村庄 y 的最短路径长度为多少,数据保证了 t 是不下降的。
输出格式
共 Q 行,对每一个询问 (x, y, t) 输出对应的答案。
如果在第 t 天无法找到从 x 村庄到 y 村庄的路径,经过若干个已重建完成的村庄,
或者村庄 x 或村庄 y 在第 t 天仍未修复完成,则输出 -1。
说明/提示
- 对于 30% 的数据,有
N ≤ 50; - 对于 30% 的数据,有
tᵢ = 0,其中有 20% 的数据有tᵢ = 0且N > 50; - 对于 50% 的数据,有
Q ≤ 100; - 对于 100% 的数据,有
1 ≤ N ≤ 200,0 ≤ M ≤ N(N−1)/2,1 ≤ Q ≤ 50000, 所有输入数据涉及整数均不超过10⁵。
时限 1 秒,内存 125 MB。
输入输出样例
输入
4 5 1 2 3 4 0 2 1 2 3 1 3 1 2 2 1 4 0 3 5 4 2 0 2 0 1 2 0 1 3 0 1 4
输出
-1 -1 5 4
四个村庄,修好的日子是 1 2 3 4。
(2, 0, 2):2 号村庄第 3 天才好 ⇒ −1(⚠ 而0—2之间明明有一条长 1 的公路 —— 这一条正是第 ② 步那个错法栽的地方);(0, 1, 2):只有 0、1 号修好了,它们之间没有直连 ⇒ −1;(0, 1, 3):2 号也好了,0 →(1) 2 →(4) 1= 5;(0, 1, 4):3 号也好了,0 →(1) 2 →(1) 3 →(2) 1= 4。
1第一版:每个询问都从头跑一次最短路
// 第一版 = 参照物:每个询问都在「已经修好的村庄」这张子图上,从头跑一次 Dijkstra//// 这是绝大多数人的第一反应,而且它**是对的** —— 问题只有一个:太慢。// 每次 O(N²),Q 次就是 50000 × 200² = **2 × 10⁹**。// ⇒ 它同时也是这一页的对拍参照物:一行 Floyd 都没有,两条路完全独立。
#include <bits/stdc++.h>using namespace std;const int INF = 0x3f3f3f3f;
int g[205][205], t[205];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; for (int i = 0; i < n; i++) cin >> t[i]; for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) g[i][j] = (i == j ? 0 : INF); for (int e = 0; e < m; e++) { int i, j, w; cin >> i >> j >> w; g[i][j] = min(g[i][j], w); g[j][i] = min(g[j][i], w); } int q; cin >> q; while (q--) { int x, y, tt; cin >> x >> y >> tt; if (t[x] > tt || t[y] > tt) { cout << -1 << "\n"; continue; } vector<int> dist(n, INF); vector<char> done(n, 0); dist[x] = 0; for (int it = 0; it < n; it++) { int u = -1; for (int i = 0; i < n; i++) if (!done[i] && t[i] <= tt && dist[i] < INF && (u < 0 || dist[i] < dist[u])) u = i; if (u < 0) break; done[u] = 1; for (int v = 0; v < n; v++) if (t[v] <= tt && g[u][v] < INF && dist[u] + g[u][v] < dist[v]) dist[v] = dist[u] + g[u][v]; } cout << (dist[y] >= INF ? -1 : dist[y]) << "\n"; } return 0;}点「运行 ▶」看结果
这是绝大多数人的第一反应,而且它是对的:把「第 t 天已修好的村庄」当成一张子图,跑一次 Dijkstra。
顶格 N = 200、Q = 50000 |
工作量(次数,机器无关) | 本机秒表(A 机 · WSL2 · 2026-08-31) |
|---|---|---|
每询问一次 O(N²) Dijkstra |
Q × N² = 2 × 10⁹ |
8.73 秒(时限 1 秒) |
2★★★ 关键的一步:Floyd 的 k 循环,本来就是「一个个加中转站」
本章第 3 步把 Floyd 的本体写清楚了:
f[k][i][j] = 只允许拿前 k 个点当中转站时,i 到 j 的最短距离—— 而这道题的村庄正好是一个个修好的,修好一个,就是「允许多拿一个点当中转站」。
于是题面白送的两句保证接上了:
| 题面那句话 | 它买到了什么 |
|---|---|
t₀ ≤ t₁ ≤ … ≤ t_{N−1} |
村庄修好的顺序就是编号顺序 ⇒ k 顺着 0, 1, 2, … 加就行 |
询问的 t 不下降 |
那个 k 指针只往前走,从不回头 |
⇒ 整道题一共只跑一遍 Floyd,总复杂度 O(N³ + Q)。
| 工作量 | 本机秒表 | |
|---|---|---|
| ★ 增量 Floyd | N³ = 8 × 10⁶ |
★ 0.11 秒 |
| 每询问一次 Dijkstra | Q × N² = 2 × 10⁹ |
8.73 秒 |
★ 250 倍。 而代码比第一版还短。
// 洛谷 P1119 灾后重建 —— ★ 这一版就能 AC//// ★★★ 关键的一步:Floyd 的 k 循环**本来就是「允许中转的点一个个加进去」**,// 而这道题的村庄**正好也是一个个修好的** —— 两件事是同一件事。//// 题面白送了两句保证,缺一不可:// ① t[0] ≤ t[1] ≤ … ≤ t[N−1] ⇒ 村庄修好的顺序就是编号顺序 ⇒ k 顺着 0..N−1 加// ② 询问的 t **不下降** ⇒ 那个 k 指针**只往前走,从不回头**// ⇒ 总复杂度 O(N³ + Q),而不是每个询问一次 O(N³)。//// ⚠ 村庄编号是 **0 ~ N−1**。
#include <bits/stdc++.h>using namespace std;const int INF = 0x3f3f3f3f;
int d[205][205], t[205];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; for (int i = 0; i < n; i++) cin >> t[i]; for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) d[i][j] = (i == j ? 0 : INF); for (int e = 0; e < m; e++) { int i, j, w; cin >> i >> j >> w; d[i][j] = min(d[i][j], w); d[j][i] = min(d[j][i], w); // 公路是双向的 }
int q; cin >> q; int k = 0; // ★ 指针只往前走 while (q--) { int x, y, tt; cin >> x >> y >> tt; while (k < n && t[k] <= tt) { // ★ `<=`:第 t[k] 天当天即可通车 for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) d[i][j] = min(d[i][j], d[i][k] + d[k][j]); k++; } // ★ 两头自己都得修好了才谈得上路 —— 这一条和 d[x][y] 是两件事 if (t[x] > tt || t[y] > tt || d[x][y] >= INF) cout << -1 << "\n"; else cout << d[x][y] << "\n"; } return 0;}点「运行 ▶」看结果
3★★ 那两句保证不是背景 —— 而且它们的分量不一样
| 300 轮 | 正解(增量 Floyd) | 「每询问重来一遍」那一版 |
|---|---|---|
| ⚠ 违反「询问的 t 不下降」 | ★ 错 55 / 300 | ★ 0 / 300 |
| ⚠ 违反「tᵢ 不下降」 | ★ 错 74 / 300 | ★ 错 74 / 300 |
⇒ 两句保证的分量完全不同:
- 「询问 t 不下降」只有增量版依赖(指针回不去了)—— 对那个「每次重来」的版本它是噪声;
- 「tᵢ 不下降」两版都依赖:村庄不按编号顺序修好, 「前 k 个点」这句话本身就不再等于「已经修好的点」。
★★ 这是「同一句约束对不同的写法分量不同」的第六次现场 —— 而这一页第一次把两句约束、两个写法摆成了一张 2 × 2 的表: 一句是「题目 × 写法」都命门,另一句只对其中一个写法是命门。
4⚠ 第一个错法:忘了「两头自己也得修好」
// ✗ 错法①:只看 d[x][y],忘了「x 或 y 自己还没修好」也要输出 −1//// 题面把这一条说了两遍(描述里一遍、输出格式里又一遍):// 「……或者村庄 x 或村庄 y 在第 t 天仍未重建完成,则需要输出 −1。」//// ⚠ 它和 d[x][y] 是**两件事**:x 和 y 之间可能早就有一条直达的公路(初始化时就填进表了),// 可那两个村庄自己还是废墟。
#include <bits/stdc++.h>using namespace std;const int INF = 0x3f3f3f3f;int d[205][205], t[205];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; for (int i = 0; i < n; i++) cin >> t[i]; for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) d[i][j] = (i == j ? 0 : INF); for (int e = 0; e < m; e++) { int i, j, w; cin >> i >> j >> w; d[i][j] = min(d[i][j], w); d[j][i] = min(d[j][i], w); } int q; cin >> q; int k = 0; while (q--) { int x, y, tt; cin >> x >> y >> tt; while (k < n && t[k] <= tt) { for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) d[i][j] = min(d[i][j], d[i][k] + d[k][j]); k++; } cout << (d[x][y] >= INF ? -1 : d[x][y]) << "\n"; // ✗ 少了 t[x] / t[y] 那两句 } return 0;}点「运行 ▶」看结果
题面把这一条说了两遍(题目描述里一遍、输出格式里又一遍),
因为它和 d[x][y] 是两件事:
x和y之间可能本来就有一条直达的公路(初始化时就填进表了),可那两个村庄自己还是废墟。
官方样例第一问 (2, 0, 2) 就是专门放在这儿的:0—2 之间有一条长 1 的路,
而 2 号村庄第 3 天才修好。
| 300 轮 | 默认档 | ★ 询问的两头偏爱还没修好的村庄 |
|---|---|---|
| 「忘了两头」被抓 | 292 | ★ 299 |
5★★ 第二个错法:一条只有一天宽的线
// ✗ 错法②:把「第 t[k] 天修好」当成「第 t[k] 天还不能用」//// 题面:「……在第 tᵢ 天重建完成,**并且在当天即可通车**。」// ⇒ 判据是 `t[k] <= tt`,写成 `<` 就整整晚了一天。// ★ 这是一条**只有一天宽**的线:只有「询问的 t 正好等于某个村庄的 tᵢ」时它才现形。
#include <bits/stdc++.h>using namespace std;const int INF = 0x3f3f3f3f;int d[205][205], t[205];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; for (int i = 0; i < n; i++) cin >> t[i]; for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) d[i][j] = (i == j ? 0 : INF); for (int e = 0; e < m; e++) { int i, j, w; cin >> i >> j >> w; d[i][j] = min(d[i][j], w); d[j][i] = min(d[j][i], w); } int q; cin >> q; int k = 0; while (q--) { int x, y, tt; cin >> x >> y >> tt; while (k < n && t[k] < tt) { // ✗ 少了一个等号 for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) d[i][j] = min(d[i][j], d[i][k] + d[k][j]); k++; } if (t[x] >= tt || t[y] >= tt || d[x][y] >= INF) cout << -1 << "\n"; // ✗ 同一处 off-by-one else cout << d[x][y] << "\n"; } return 0;}点「运行 ▶」看结果
题面:「……在第 tᵢ 天重建完成,并且在当天即可通车。」⇒ 判据必须是 t[k] <= tt。
| 默认档 | ★ 「询问的 t 正好落在某个 tᵢ 上」那一档 | |
|---|---|---|
询问级:t 恰好等于某个 tᵢ |
1291 / 2700(47.8%) | ★ 2700 / 2700(100%) |
| 种子级:300 轮真被抓 | 177 | ★ 287 |
⚠ 第一层在默认档就已经有近一半的询问踩到了 —— 可真被抓只有 177 / 300,因为还要第二层:那一天修好的村庄得真的在最短路上。
⇒ 又一次「满足触发条件 ≠ 真被抓」,而这一页两层的比值是 1.6 倍 (本书量过的范围:从一个不差到差 150 倍)。
6⚠ 第三个错法:编号从 1 写起
// ✗ 错法④:编号当成 1 ~ N —— 三重循环从 1 写起//// 这道题的村庄编号是 **0 ~ N−1**,而前面整整一张题单([P1339] [P1629] [P1462] [B3647] [P3385])// 的编号都是 1 开头 —— 手感就是这么带过来的。// ⇒ 0 号村庄**永远不会被当成中转站**,而一个根本不存在的 n 号村庄被塞进了循环。//// ★ 这是[第 27 章](/sol/p1352/)那条「同一张题单里编号基会翻面」的又一次现场。
#include <bits/stdc++.h>using namespace std;const int INF = 0x3f3f3f3f;int d[205][205], t[205];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; for (int i = 0; i < n; i++) cin >> t[i]; for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) d[i][j] = (i == j ? 0 : INF); for (int e = 0; e < m; e++) { int i, j, w; cin >> i >> j >> w; d[i][j] = min(d[i][j], w); d[j][i] = min(d[j][i], w); } int q; cin >> q; int k = 1; // ✗ 从 1 号开始 while (q--) { int x, y, tt; cin >> x >> y >> tt; while (k <= n && t[k] <= tt) { // ✗ 一直数到 n(而 t[n] 根本没读进来) for (int i = 1; i <= n; i++) // ✗ 0 号村庄永远当不上中转站 for (int j = 1; j <= n; j++) d[i][j] = min(d[i][j], d[i][k] + d[k][j]); k++; } if (t[x] > tt || t[y] > tt || d[x][y] >= INF) cout << -1 << "\n"; else cout << d[x][y] << "\n"; } return 0;}点「运行 ▶」看结果
这道题的村庄编号是 0 ~ N−1,而前面整整一张题单 (P1339 / P1629 / P1462 / B3647 / P3385)的编号全是 1 开头 —— 手感就是这么带过来的。
⇒ 0 号村庄永远当不上中转站,而一个根本不存在的 n 号村庄被塞进了循环。 300 轮被抓 181(专门档 278)。
★ 这是第 27 章那条「同一张题单里编号基会翻面」的又一次现场 ——
上一次是 P2016(0 ~ n−1)挨着 P1352(1 ~ n)。
7★ 第四个版本:答案永远对,就是跑不完
// ?错法③:每个询问都把 k 指针从 0 重来 —— **答案永远是对的,就是跑不完**//// 忘了「询问的 t 不下降」这句保证的人,很自然会写成「每次重新把该修好的村庄加一遍」。// 一次是 O(N³),Q 次就是 50000 × 200³ = **4 × 10¹¹**。//// ★ 这一版是「[答案对但跑不完](/sol/p5019/)」那条线的又一个现场:// **官方样例挡不住它,对拍也挡不住它** —— 只能数次数。
#include <bits/stdc++.h>using namespace std;const int INF = 0x3f3f3f3f;int d0[205][205], d[205][205], t[205];
int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n, m; cin >> n >> m; for (int i = 0; i < n; i++) cin >> t[i]; for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) d0[i][j] = (i == j ? 0 : INF); for (int e = 0; e < m; e++) { int i, j, w; cin >> i >> j >> w; d0[i][j] = min(d0[i][j], w); d0[j][i] = min(d0[j][i], w); } int q; cin >> q; while (q--) { int x, y, tt; cin >> x >> y >> tt; memcpy(d, d0, sizeof(d0)); // ?每次从原始的表重来 for (int k = 0; k < n; k++) { if (t[k] > tt) break; for (int i = 0; i < n; i++) for (int j = 0; j < n; j++) d[i][j] = min(d[i][j], d[i][k] + d[k][j]); } if (t[x] > tt || t[y] > tt || d[x][y] >= INF) cout << -1 << "\n"; else cout << d[x][y] << "\n"; } return 0;}点「运行 ▶」看结果
忘了「询问的 t 不下降」那句保证的人,很自然会写成「每次重新把该修好的村庄加一遍」。
| 工作量 | 本机秒表(顶格) | 300 轮和正解不一致 | |
|---|---|---|---|
| ★ 增量 Floyd | N³ = 8 × 10⁶ |
0.11 秒 | —— |
| 每询问一次 Dijkstra | Q × N² = 2 × 10⁹ |
8.73 秒 | 0 轮 |
| 每询问把 k 从头加一遍 | Q × N³ = ★ 4 × 10¹¹ |
★ 176.3 秒 | ★ 0 轮 |
⇒ 又一次「答案对但跑不完」:官方样例挡不住它,对拍也挡不住它 —— 这一类只能靠数次数发现。 ⚠ 而它有一个反直觉的好处(上面第 ③ 步那张表):它是唯一不依赖「询问 t 不下降」的版本。
8★ 对拍这一页
参照物就是第一版(每询问一次 Dijkstra):一行 Floyd 都没有,两条路完全独立。
300 轮(n 随机 5~8) |
默认档 | ★ 落在 tᵢ 上 | ★ 两头没修好 | ⚠ 询问 t 乱序 | ⚠ tᵢ 乱序 |
|---|---|---|---|---|---|
| 增量 Floyd ≡ 每询问 Dijkstra | ★ 0 | ★ 0 | ★ 0 | ⚠ 55 | ⚠ 74 |
| 每询问重来一遍 | ★ 0 | ★ 0 | ★ 0 | ★ 0 | ⚠ 74 |
| 忘了两头也要修好 | 292 | 196 | ★ 299 | 299 | 289 |
<= 写成 < |
177 | ★ 287 | 116 | 177 | 194 |
| 编号从 1 写起 | 181 | 278 | 131 | 224 | 176 |
| ⚠ 这一档答案是 −1 的询问数 | 1680 / 2624 | 620 / 2624 | 2044 / 2624 | 1701 | 1669 |
⚠ 最后一行照例是读法说明:「两头没修好」那一档有 78% 的询问答案就是 −1,
于是「<= 写成 <」在那一档反而掉到 116 —— 一档只能护着一部分 bug。
★ 顺带一道三十秒的算术:所有整数 ≤ 10⁵、路径最多 199 条边 ⇒ 答案上界 19 900 000,
int 余量 108 倍 ⇒ 不用 long long。
9度量程序和生成器
10一页纸
| ★★★ 关键的一步 | 村庄一个个修好 = Floyd 的 k 一层层加进去 ⇒ 全程只跑一遍 Floyd |
| ★ 值多少 | 8 × 10⁶ vs 2 × 10⁹,0.11 秒 vs 8.73 秒(250 倍),而代码更短 |
| ★★★ 两句保证 | 「询问 t 不下降」只有增量版依赖(另一版 0 / 300)、「tᵢ 不下降」两版都依赖(各 74 / 300) |
| ⚠ 忘了两头 | d[x][y] 有值 ≠ 两头修好了 —— 抓 292,官方样例第一问就是为它放的 |
| ★★ 「当天即可通车」 | <= 写成 <;⚠ 触发条件两层,第一层默认档就 47.8%,真被抓 177 ⇒ 1.6 倍 |
| ⚠ 编号基 | 0 ~ N−1,而同一张题单前五道全是 1 开头 —— 抓 181 |
| ★ 每询问重来 | 答案 0 轮不同,顶格 176.3 秒(时限 1 秒)⇒ 只能数次数才发现 |