题单 · 习题解析

洛谷 P1113 [USACO02FEB] 杂务

★★★ 题面「前置只可能在 1 至 k−1 中」⇒ 输入本身就是拓扑序 ⇒ 一遍扫,**一条边都不用存**;★★ 打乱顺序:一遍扫被抓 187/300 而拓扑排序 0(命门 vs 噪声);★ 两个错法各占一边:写成 f[n] 恒 ≤ 正解(样例放过)、max 写成求和恒 ≥ 正解(样例挡住)

原题:洛谷 P1113出自 第 29 章 图的存储:三种存法的对比与选型 的题单出自 第 31 章 拓扑排序 的题单题面本地存档:2026-08-30
⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目描述

John 的农场在给奶牛挤奶前有很多杂务要完成,每一项杂务都需要一定的时间来完成它。 比如:他们要将奶牛集合起来,将他们赶进牛棚,为奶牛清洗乳房以及一些其它工作。 尽早将所有杂务完成是必要的,因为这样才有更多时间挤出更多的牛奶。

当然,有些杂务必须在另一些杂务完成的情况下才能进行。比如:只有将奶牛赶进牛棚才能开始为它清洗乳房, 还有在未给奶牛清洗乳房之前不能挤奶。我们把这些工作称为完成本项工作的准备工作。 至少有一项杂务不要求有准备工作,这个可以最早着手完成的工作,标记为杂务 1

John 有需要完成的 n 个杂务的清单,并且这份清单是有一定顺序的, 杂务 k (k > 1) 的准备工作只可能在杂务 1k−1

写一个程序依次读入每个杂务的工作说明。计算出所有杂务都被完成的最短时间。 当然互相没有关系的杂务可以同时工作,并且,你可以假定 John 的农场有足够多的工人来同时完成任意多项任务。

输入格式

第 1 行,一个整数 n (3 ≤ n ≤ 10 000),必须完成的杂务的数目;

第 2 至 n+1 行,每行有一些用空格隔开的整数,分别表示:

  • 工作序号(保证在输入文件中是从 1n 有序递增的);
  • 完成工作所需要的时间 len (1 ≤ len ≤ 100)
  • 一些必须完成的准备工作,总数不超过 100 个,由一个数字 0 结束。 有些杂务没有需要准备的工作只描述一个单独的 0

保证整个输入文件中不会出现多余的空格。

输出格式

一个整数,表示完成所有杂务所需的最短时间。

输入输出样例

输入

7
1 5 0
2 2 1 0
3 3 2 0
4 6 1 0
5 1 2 4 0
6 8 2 4 0
7 4 3 5 6 0

输出

23

f[1] = 5f[2] = 2 + f[1] = 7f[3] = 3 + f[2] = 10f[4] = 6 + f[1] = 11f[5] = 1 + max(f[2], f[4]) = 12f[6] = 8 + max(f[2], f[4]) = 19f[7] = 4 + max(f[3], f[5], f[6]) = 23

⚠ 注意这组样例里最晚完成的恰好就是第 7 项 —— 于是第 ③ 步那个「答案写成 f[n]」的错法, 它一个字都没挡住。

1★★★ 先读一遍题面那句话:它把这道题从「图论题」降级成了「一遍扫」

「杂务 k (k > 1) 的准备工作只可能在杂务 1k−1。」

★★ 这句话的意思是:输入本身就已经是一个拓扑序了

1 → n 的顺序处理,每读到杂务 i,它的所有前置 j < i 早就算完了。于是:

    f[i] = len[i] + max(f[j])     j 是 i 的前置;没有前置就是 f[i] = len[i]
    答案 = max(f[i])              ⚠ 不是 f[n]

⇒ ★★ 一条边都不用存。 前置编号读进来就地拿去取 max,用完就扔。

这是第 29 章那句话的另一面 ——「存法没有绝对的好坏,只有配不配得上你要问的问题」, 而「不存」也是一种存法,在这道题上它就是最好的那种:

顶格 n = 10⁴、每项 100 个前置
前置关系一共 994 950
建图那版要存它们(vector<vector<int>> 4.02 MB(实测常驻 10.2 MB)
★ 正解要存 0 条(实测常驻 4.4 MB)
p1113.cpp★★ 一遍扫过去,连图都不用存
// ★★ P1113 正解:**一遍扫过去就行 —— 连图都不用存。**
//
// 题面里那句话是全部关键:
// 「杂务 k (k > 1) 的准备工作**只可能在杂务 1 至 k−1 中**」
// ⇒ **输入本身就已经是一个拓扑序了。** 按 1 → n 的顺序处理,
// 每读到杂务 i,它的所有前置 j < i 早就算完了。
//
// f[i] = len[i] + max(f[j]) j 是 i 的前置;没有前置就是 f[i] = len[i]
// 答案 = max(f[i]) ⚠ 不是 f[n]!见 p1113Last.cpp
//
// ⇒ ★★ 这是第 29 章那句话的另一面:**「选存法」里包括「不存」。**
// 前置关系读进来就地用掉,一条边都不留 —— 顶格连 1 KB 都不到,
// 而老老实实建图要存 10⁶ 条边(8 MB,见 p1113Topo.cpp)。
//
// ⚠ 输入是**变长行**:`序号 用时 前置… 0`。别去管换行,直接流式读到 0 为止。
// 规模:n ≤ 10⁴、len ≤ 100 ⇒ 答案 ≤ 10⁶,**int 绰绰有余**。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
vector<int> f(n + 1, 0);
int ans = 0;
for (int i = 1; i <= n; i++) {
int id, len;
cin >> id >> len;
int best = 0;
int p;
while (cin >> p && p != 0) best = max(best, f[p]); // ★ 前置早就算完了
f[id] = best + len;
ans = max(ans, f[id]); // ★★ 答案是全体的 max
}
printf("%d\n", ans);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

2★ 另一条路:真的建图 + 拓扑排序 —— 留着它是为了称那句话的重量

p1113Topo.cpp★ 建图 + Kahn 拓扑排序(对拍参照物)

它当然也对,而且不依赖题面那句话。所以它正好是本页的两件工具:对拍的参照物(和正解一行代码都不共享), 以及称那句约束分量的秤

★★★ 把杂务的输出顺序打乱(同一张 DAG,只是不再保证 1..k−1)
300 轮 照题面 打乱输出顺序(违反题面那句)
「一遍扫」那版(正解) 0 被抓 187
拓扑排序那版 0 0(它根本不在乎顺序)

⇒ 按第 12 章那套三分法,这句「准备工作只可能在 1 至 k−1 中」 对「一遍扫」那版是命门,对拓扑排序那版是噪声。 ★★ 又一次:「这句约束重不重要」是「题目 × 你写的那一版」的属性第 28 章 P1171本轮 B3643 之后,第三次)。

★ 而且被抓的那 187 轮里,「一遍扫」300 / 300 轮都 ≤ 正解 —— 因为它把还没算过的前置当成了 0只会少算,不会多算

3⚠ 错法一:答案写成 f[n] —— 官方样例放过了它

p1113Last.cpp✗ 输出 f[n](样例照样打 23)
// ✗ P1113 错法一:**把答案写成 f[n]** —— 而官方样例正好放过它。
//
// 「完成所有杂务的最短时间」= 所有杂务里最晚结束的那个,也就是 max(f[i])。
// 写成 f[n] 就是默认「最后一项一定最晚完成」—— 题面**一个字都没这么保证**。
//
// ★ 它解的是一个更小的问题(只要求做完第 n 项)⇒ 答案**恒 ≤ 正解**。
// ⚠ 而官方样例里 f[7] 恰好就是 23(第 7 项确实最晚),**样例挡不住它**。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
vector<int> f(n + 1, 0);
for (int i = 1; i <= n; i++) {
int id, len;
cin >> id >> len;
int best = 0, p;
while (cin >> p && p != 0) best = max(best, f[p]);
f[id] = best + len;
}
printf("%d\n", f[n]); // ★ 这里
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★ 「最后一项一定最晚完成」—— 题面一个字都没这么保证

它解的是一个更小的问题(只要求做完第 n 项)⇒ 答案恒 ≤ 正解300 / 300), 对拍被抓 183 / 300,最惨的一轮少算了 98.05%

⚠ 而官方样例里 f[7] 恰好就是 23 —— 样例一个字都没挡住。 这又是那条规律的正面:官方样例是个「一测就死」的过滤器, 它挡住的是「每组都错」的,放过的是「偶尔才错」的(这个错法只错六成)。

4错法二:把 max 写成求和 —— 这个样例当场就挡住了

p1113Sum.cpp✗ f[i] = len + Σf[前置](样例打 59)
// ✗ P1113 错法二:`f[i] = len[i] + **Σ** f[前置]` —— 把「等所有前置都好」读成了「一件件排队做」。
//
// 题面写着「互相没有关系的杂务**可以同时工作**,而且工人要多少有多少」,
// 所以一项杂务的开工时刻是**所有前置里最晚的那个**(max),不是它们的总和。
//
// ★ 它给出的是一个**真做得完的排法**(把所有前置串起来做)⇒ 答案**恒 ≥ 正解**。
// ⚠ 而且它错得**离谱**:前置一多就指数级放大 —— 解析页第 ③ 步量了差多少。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
vector<long long> f(n + 1, 0);
long long ans = 0;
for (int i = 1; i <= n; i++) {
int id, len, p;
cin >> id >> len;
long long sum = 0;
while (cin >> p && p != 0) sum += f[p]; // ★ 加,而不是 max
f[id] = sum + len;
ans = max(ans, f[id]);
}
printf("%lld\n", ans);
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

题面写着「互相没有关系的杂务可以同时工作,而且工人要多少有多少」—— 所以一项杂务的开工时刻是所有前置里最晚的那个max),不是它们的总和。

★ 它给出的是一个真做得完的排法(把所有前置串起来做)⇒ 恒 正解(300 / 300), 对拍被抓 169 / 300,最多多算 124.40%

★ 两个错法正好各占一边 —— 而这两个方向都是不用跑就能判的

第 26 章那一轮总结过一条判据:

  • 解的是一个放宽了 / 缩小了的问题 ⇒ 答案恒偏小(这里是「只要求做完第 n 项」);
  • 给出的是一个真做得到的方案 ⇒ 答案恒偏大(这里是「把前置一件件串着做」)。

⇒ 这一页两个方向都齐了,而且两条界都实测 300 / 300

5⚠ 和算法无关但会挂人的那一条:变长行的读入

每一行是 序号 用时 前置… 0行的长度不固定

    int p;
    while (cin >> p && p != 0) best = max(best, f[p]);

别去管换行cin >> 本来就跳过所有空白(空格和换行一视同仁), 0 是这一行的终止符,读到它就停 —— 整个输入当成一条数字流读就对了。

顶格那组数据实测 4 693 096 字节(4.48 MB)、约 10⁶ 个整数, 而时限 1 秒 ⇒ 实测两版都是 0.02 ~ 0.03 秒A 机 · WSL2 · 2026-08-30,独占)。 ★ 又一道三十秒的算术题,答案还是「够」。

6★ 对拍这一页

300 轮(n 随机 3~10,参照物 = 拓扑排序)
正解 ≡ 拓扑排序 不一致 0 轮
答案写成 f[n] 183(恒 ≤ 正解,最多少算 98.05%)
max 写成求和 169(恒 ≥ 正解,最多多算 124.40%)
⚠ 打乱输入顺序那一档:正解 187(而拓扑排序仍是 0)

7度量程序和生成器

p1113Count.cpp度量程序(本页所有数字都出自它)
p1113Gen.cpp数据生成器

8一页纸

★★★ 关键的一步 题面「前置只可能在 1 至 k−1 中」⇒ 输入本身就是拓扑序 ⇒ 一遍扫,一条边都不用存
★★ 存法的另一面 建图版 4.02 MB / 常驻 10.2 MB,正解 0 条边 / 常驻 4.4 MB ——「不存」也是一种存法
★★★ 那句约束的分量 打乱顺序:一遍扫被抓 187 / 300,拓扑排序 0 ⇒ 对一版是命门、对另一版是噪声
⚠ 错法一 答案写成 f[n] ⇒ 恒 ≤ 正解(300/300),官方样例放过了它(183/300 才错)
错法二 max 写成求和 ⇒ 恒 ≥ 正解(300/300),样例当场挡住(59 vs 23)
★ 两个方向都不用跑就能判 放宽/缩小问题 ⇒ 偏小;给出一个真方案 ⇒ 偏大
规模 顶格 4.48 MB 输入、答案上界 10⁶ ⇒ int 够,两版都 0.02~0.03 秒