0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1113,日期见页头。两边不一致时信原站。
题目描述
John 的农场在给奶牛挤奶前有很多杂务要完成,每一项杂务都需要一定的时间来完成它。 比如:他们要将奶牛集合起来,将他们赶进牛棚,为奶牛清洗乳房以及一些其它工作。 尽早将所有杂务完成是必要的,因为这样才有更多时间挤出更多的牛奶。
当然,有些杂务必须在另一些杂务完成的情况下才能进行。比如:只有将奶牛赶进牛棚才能开始为它清洗乳房,
还有在未给奶牛清洗乳房之前不能挤奶。我们把这些工作称为完成本项工作的准备工作。
至少有一项杂务不要求有准备工作,这个可以最早着手完成的工作,标记为杂务 1。
John 有需要完成的 n 个杂务的清单,并且这份清单是有一定顺序的,
杂务 k (k > 1) 的准备工作只可能在杂务 1 至 k−1 中。
写一个程序依次读入每个杂务的工作说明。计算出所有杂务都被完成的最短时间。 当然互相没有关系的杂务可以同时工作,并且,你可以假定 John 的农场有足够多的工人来同时完成任意多项任务。
输入格式
第 1 行,一个整数 n (3 ≤ n ≤ 10 000),必须完成的杂务的数目;
第 2 至 n+1 行,每行有一些用空格隔开的整数,分别表示:
- 工作序号(保证在输入文件中是从
1到n有序递增的); - 完成工作所需要的时间
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] = 5;f[2] = 2 + f[1] = 7;f[3] = 3 + f[2] = 10;f[4] = 6 + f[1] = 11;
f[5] = 1 + max(f[2], f[4]) = 12;f[6] = 8 + max(f[2], f[4]) = 19;
f[7] = 4 + max(f[3], f[5], f[6]) = 23。
⚠ 注意这组样例里最晚完成的恰好就是第 7 项 —— 于是第 ③ 步那个「答案写成 f[n]」的错法,
它一个字都没挡住。
1★★★ 先读一遍题面那句话:它把这道题从「图论题」降级成了「一遍扫」
「杂务
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]⇒ ★★ 一条边都不用存。 前置编号读进来就地拿去取 max,用完就扔。
这是第 29 章那句话的另一面 ——「存法没有绝对的好坏,只有配不配得上你要问的问题」, 而「不存」也是一种存法,在这道题上它就是最好的那种:
顶格 n = 10⁴、每项 100 个前置 |
|
|---|---|
| 前置关系一共 | 994 950 条 |
建图那版要存它们(vector<vector<int>>) |
4.02 MB(实测常驻 10.2 MB) |
| ★ 正解要存 | ★ 0 条(实测常驻 4.4 MB) |
// ★★ 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;}点「运行 ▶」看结果
2★ 另一条路:真的建图 + 拓扑排序 —— 留着它是为了称那句话的重量
它当然也对,而且不依赖题面那句话。所以它正好是本页的两件工具:对拍的参照物(和正解一行代码都不共享), 以及称那句约束分量的秤。
| 300 轮 | 照题面 | 打乱输出顺序(违反题面那句) |
|---|---|---|
| 「一遍扫」那版(正解) | ★ 0 | ★ 被抓 187 |
| 拓扑排序那版 | 0 | ★ 0(它根本不在乎顺序) |
⇒ 按第 12 章那套三分法,这句「准备工作只可能在 1 至 k−1 中」 对「一遍扫」那版是命门,对拓扑排序那版是噪声。 ★★ 又一次:「这句约束重不重要」是「题目 × 你写的那一版」的属性 (第 28 章 P1171、本轮 B3643 之后,第三次)。
★ 而且被抓的那 187 轮里,「一遍扫」300 / 300 轮都 ≤ 正解 ——
因为它把还没算过的前置当成了 0,只会少算,不会多算。
3⚠ 错法一:答案写成 f[n] —— 官方样例放过了它
// ✗ 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;}点「运行 ▶」看结果
它解的是一个更小的问题(只要求做完第 n 项)⇒ 答案恒 ≤ 正解(300 / 300),
对拍被抓 183 / 300,最惨的一轮少算了 98.05%。
⚠ 而官方样例里 f[7] 恰好就是 23 —— 样例一个字都没挡住。
这又是那条规律的正面:官方样例是个「一测就死」的过滤器,
它挡住的是「每组都错」的,放过的是「偶尔才错」的(这个错法只错六成)。
4错法二:把 max 写成求和 —— 这个样例当场就挡住了
// ✗ 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;}点「运行 ▶」看结果
题面写着「互相没有关系的杂务可以同时工作,而且工人要多少有多少」——
所以一项杂务的开工时刻是所有前置里最晚的那个(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度量程序和生成器
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 秒 |