题单 · 习题解析

洛谷 P1119 灾后重建

★★★ 村庄一个个修好 = Floyd 的 k 一层层加进去 ⇒ 全程只跑**一遍** Floyd(`0.11 秒 vs 8.73 秒`,250 倍,代码还更短);★★★ 题面那**两句保证**分量不同 —— 「询问 t 不下降」**只有增量版依赖**(另一版 0 / 300)、「tᵢ 不下降」**两版都依赖**(各 74 / 300);⚠ 「两头自己也得修好」和 `d[x][y]` 是两件事(抓 292,样例第一问就为它放的);★★ 「当天即可通车」那个 `<=`:触发条件两层,第一层 47.8% 而真被抓 177;★ 「每询问重来一遍」答案 **0 轮不同**、顶格 **176.3 秒** ⇒ 只能数次数

原题:洛谷 P1119出自 第 33 章 最短路二:Floyd、Bellman-Ford、SPFA 与负环 的题单题面本地存档:2026-08-31
⚠ 先自己写一遍,再往下看

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

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, ww 不超过 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ᵢ = 0N > 50
  • 对于 50% 的数据,有 Q ≤ 100
  • 对于 100% 的数据,有 1 ≤ N ≤ 2000 ≤ M ≤ N(N−1)/21 ≤ 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第一版:每个询问都从头跑一次最短路

p1119Brute.cpp第一版:每询问一次 Dijkstra —— 答案全对,顶格 8.73 秒
// 第一版 = 参照物:每个询问都在「已经修好的村庄」这张子图上,从头跑一次 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

这是绝大多数人的第一反应,而且它是对的:把「第 t 天已修好的村庄」当成一张子图,跑一次 Dijkstra。

顶格 N = 200Q = 50000 工作量(次数,机器无关) 本机秒表(A 机 · WSL2 · 2026-08-31)
每询问一次 O(N²) Dijkstra Q × N² = 2 × 10⁹ 8.73 秒(时限 1 秒)

2★★★ 关键的一步:Floyd 的 k 循环,本来就是「一个个加中转站」

★★★ 村庄一个个修好,和 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 = 8 × 10⁶ 0.11 秒
每询问一次 Dijkstra Q × N² = 2 × 10⁹ 8.73 秒

250 倍。 而代码比第一版还短。

p1119.cpp★ 这一版就能 AC(增量 Floyd,顶格 0.11 秒)
// 洛谷 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

3★★ 那两句保证不是背景 —— 而且它们的分量不一样

★★★ 造一档违反它的数据,看谁的行为变了 —— 两句保证,各被谁依赖
300 轮 正解(增量 Floyd) 「每询问重来一遍」那一版
⚠ 违反「询问的 t 不下降 错 55 / 300 0 / 300
⚠ 违反「tᵢ 不下降 错 74 / 300 错 74 / 300

两句保证的分量完全不同

  • 「询问 t 不下降」只有增量版依赖(指针回不去了)—— 对那个「每次重来」的版本它是噪声
  • 「tᵢ 不下降」两版都依赖:村庄不按编号顺序修好, 「前 k 个点」这句话本身就不再等于「已经修好的点」。

★★ 这是「同一句约束对不同的写法分量不同」的第六次现场 —— 而这一页第一次把两句约束、两个写法摆成了一张 2 × 2 的表: 一句是「题目 × 写法」都命门,另一句只对其中一个写法是命门。

4⚠ 第一个错法:忘了「两头自己也得修好」

p1119NotReady.cpp✗ 只看 d[x][y](样例第一问就打出 1,答案是 −1)
// ✗ 错法①:只看 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

题面把这一条说了两遍(题目描述里一遍、输出格式里又一遍), 因为它和 d[x][y]两件事

xy 之间可能本来就有一条直达的公路(初始化时就填进表了),可那两个村庄自己还是废墟。

官方样例第一问 (2, 0, 2) 就是专门放在这儿的:0—2 之间有一条长 1 的路, 而 2 号村庄第 3 天才修好。

300 轮 默认档 ★ 询问的两头偏爱还没修好的村庄
「忘了两头」被抓 292 299

5★★ 第二个错法:一条只有一天宽的线

p1119Strict.cpp✗ t[k] <= tt 写成 <(样例第三问打 −1,答案是 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

题面:「……在第 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 写起

p1119Base.cpp✗ 三重循环从 1 到 n(样例四问全打 −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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

这道题的村庄编号是 0 ~ N−1,而前面整整一张题单 (P1339 / P1629 / P1462 / B3647 / P3385)的编号全是 1 开头 —— 手感就是这么带过来的。

⇒ 0 号村庄永远当不上中转站,而一个根本不存在的 n 号村庄被塞进了循环。 300 轮被抓 181(专门档 278)。

★ 这是第 27 章那条同一张题单里编号基会翻面」的又一次现场 —— 上一次是 P2016(0 ~ n−1)挨着 P1352(1 ~ n)。

7★ 第四个版本:答案永远对,就是跑不完

p1119Reset.cpp?每询问把 k 从 0 重来 —— 答案全对,顶格 176.3 秒
// ?错法③:每个询问都把 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

忘了「询问的 t 不下降」那句保证的人,很自然会写成「每次重新把该修好的村庄加一遍」。

工作量 本机秒表(顶格) 300 轮和正解不一致
★ 增量 Floyd = 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 000int 余量 108 倍不用 long long

9度量程序和生成器

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

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 秒)⇒ 只能数次数才发现