0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1855,日期见页头。两边不一致时信原站。
题目描述
洛谷 2 的团队功能是其他任何 OJ 和工具难以达到的。借助洛谷强大的服务器资源, 任何学校都可以在洛谷上零成本的搭建 OJ 并高效率的完成训练计划。
为什么说是搭建 OJ 呢?为什么高效呢?

因为,你可以上传私有题目,团队外别人是无法看到的。我们还能帮你们评测!
你可以创建作业,给组员布置任务,查看组员的完成情况,还可以点评任意一份代码!
你可以创建比赛!既可以是 OI 赛制还可以是 ICPC 赛制!既可以是团队内部的私有比赛, 也可以公开赛,甚至可以指定谁可以参加比赛。这样,搞「x 校联赛」最合适不过了。 洛谷凭借这个功能,希望能够提供公开及私有比赛的另外一个平台。

值得说明的是,本次比赛就是采用团队私有题目 + 邀请比赛的机制。
洛谷的运营组决定,如果一名 OIer 向他的教练推荐洛谷,并能够成功的使用 (成功使用的定义是:该团队有 20 个或以上的成员,上传 10 道以上的私有题目, 布置过一次作业并成功举办过一次公开比赛), 那么他可以浪费掉 kkksc03 的一些时间的同时消耗掉 kkksc03 的一些金钱以满足自己的一个愿望。
kkksc03 的时间和金钱是有限的,所以他很难满足所有同学的愿望。 所以他想知道在自己的能力范围内,最多可以完成多少同学的愿望?
输入格式
第一行三个整数 n, M, T,表示一共有 n(1 ≤ n ≤ 100)个愿望,
kkksc03 的手上还剩 M(0 ≤ M ≤ 200)元,他的暑假有 T(0 ≤ T ≤ 200)分钟时间。
第 2 ~ n+1 行 mᵢ,tᵢ 表示第 i 个愿望所需要的金钱和时间。
输出格式
一行,一个数,表示 kkksc03 最多可以实现愿望的个数。
输入输出样例
输入
6 10 10 1 1 2 3 3 2 2 5 5 2 4 3
输出
4
六个愿望、10 元、10 分钟。取 (1,1)、(2,3)、(3,2)、(4,3) 这四个:
一共 10 元、9 分钟,两样都没超 ⇒ 4 个。
★ 五个为什么不行:钱最少的五个是 1+2+2+3+4 = 12 元 > 10,怎么挑都超。
★ 这一组样例只挡住了四个错法里的一个(外层正序打出 10)—— 贪心、合并费用、内层正序三个全都照样打出 4。
1第一版:几乎所有人的第一反应是贪心 —— 「先挑便宜的」
题目只数件数(每个愿望都值 1 分),所以第一反应特别顺:先做代价小的那些,能多做几个。
// ✗ P1855 的第一版:贪心 —— 「先挑便宜的」。//// 这是绝大多数人真实的第一反应,而且它的理由听着很顺:// 既然只数**件数**(每个愿望都值 1 分),那当然先做代价小的那些。//// ⚠ 问题在于「代价小」没有主语:这里有**两种**代价。// 这一版把它们加起来当成一把尺子(`m + t` 升序),能塞就塞。//// ★ 它到底错多少,页面上量过:不是「经常错得离谱」,而是「偶尔错一点点」——// 这正是这类上当最难自查的地方。
#include <bits/stdc++.h>using namespace std;
int main() { int n, M, T; if (scanf("%d %d %d", &n, &M, &T) != 3) return 0; vector<pair<int, int>> a(n); for (int i = 0; i < n; i++) if (scanf("%d %d", &a[i].first, &a[i].second) != 2) return 0;
sort(a.begin(), a.end(), [](const pair<int, int>& x, const pair<int, int>& y) { return x.first + x.second < y.first + y.second; // 两种代价加起来当一把尺子 });
int cnt = 0; for (int i = 0; i < n; i++) if (a[i].first <= M && a[i].second <= T) { M -= a[i].first; T -= a[i].second; cnt++; } printf("%d\n", cnt); return 0;}点「运行 ▶」看结果
样例上它打出 4,和正解一样。⚠ 而这正是这个上当最难自查的地方。
(1, 9) 和 (5, 5) 谁更便宜?按 m + t 排,前者 10、后者 10,一样便宜;
可如果钱只剩 3 元、时间还有一整天,那两件里只有第一件拿得动。
两个上限是各自独立的,一把尺子量不出来。
2★ 那它到底错多少 —— 「多久错一次」和「错的时候差多少」是两件事
| 比值(容量 ÷ 费用上限) | 1 | 2 | 4 | 8 | 16 |
|---|---|---|---|---|---|
| 贪心被抓 | 1 | 9 | 13 | ★ 28 | 23 |
| 错的时候平均少 | 50.00% | 41.66% | 27.73% | 23.08% | ★ 17.62% |
| ⚠ 而正解本身就是 0 的轮数 | ★ 137 | 59 | 35 | 22 | 5 |
★ 两条曲线形状不同:抓获率一路上升、到第 8 档见顶(28)之后回落到 23, 而「错的时候差多少」是一路往下的 —— 和第 20 章 P1048 那页量出来的形状一模一样。 ⇒ 「多久错一次」和「错的时候差多少」别混成一个数。
⚠⚠ 第三行是这张表最该看的一行:比值 1 那一档 300 轮里有 137 轮正解本身就是 0 (钱和时间都太少,一个愿望都实现不了)。那 137 轮里「两版一致」验的是零。 ⇒ 第 14 章 P1746 那条「一致有两种:都算对了,和都没算」的又一次现场。
3第二版:把两种费用加起来,退回一维背包
第二个特别顺的想法:既然都是代价,把 m + t 当成一个重量、M + T 当成容量,
第 23 章那份一维 01 背包一个字都不用改。
// ✗ P1855 的第二版:把两种费用**加起来**,退回一维背包。//// 想法是:「反正都是代价,`m + t` 一起算,容量就是 `M + T`」——// 这样第 23 章那份一维 01 背包一个字都不用改。//// ⚠ 它错在**两个上限是各自独立的**:// `m` 花超了不能拿 `t` 的余额去垫。合并之后这堵墙就没了。//// ★ 它有一条能写下来的性质:**答案恒 ≥ 正解**(只会高估,绝不会低估)——// 因为每一组合法方案在合并之后仍然合法(`Σm ≤ M` 且 `Σt ≤ T` ⇒ `Σ(m+t) ≤ M+T`),// 而反过来不成立。页面上那张表就是拿这条对出来的。
#include <bits/stdc++.h>using namespace std;
int f[405]; // 容量 M + T ≤ 400
int main() { int n, M, T; if (scanf("%d %d %d", &n, &M, &T) != 3) return 0; int cap = M + T; for (int i = 0; i < n; i++) { int m, t; if (scanf("%d %d", &m, &t) != 2) return 0; for (int j = cap; j >= m + t; j--) f[j] = max(f[j], f[j - m - t] + 1); } printf("%d\n", f[cap]); return 0;}点「运行 ▶」看结果
样例上它也打出 4。
每一组合法方案在合并之后仍然合法(Σm ≤ M 且 Σt ≤ T ⇒ Σ(m+t) ≤ M+T),
反过来不成立 —— 合并之后,钱花超的那部分可以拿时间的余额去垫。
| 300 轮里「它的答案 ≥ 正解」 | ★ 300 / 300 |
| 默认档里它被抓 | 148 / 300 |
⇒ 它只会高估,绝不会低估 —— 所以在洛谷上交它,得到的是「答案偏大」的 WA, 而不是「有时大有时小」。说清楚一个 bug 算了什么,它的所有表现都是白送的推论。
4★ 正解:多一维费用,就多一层循环
第 25 章正文第 4 步那句口诀,原样搬过来 —— 这道题价值全是 1(只数件数):
for 每个愿望 i
for j = M .. m[i] ← 钱,倒序
for k = T .. t[i] ← 时间,倒序
f[j][k] = max(f[j][k], f[j-m][k-t] + 1)
// P1855 榨取kkksc03 —— ★ 这一版就能 AC。//// 二维费用 01 背包的**裸题**:每个愿望要同时付「钱」和「时间」两种代价,// 而**价值全是 1**(题目只问「最多实现几个」)。//// 所以第 25 章那句口诀原样搬过来,只是内层多了一维:// for 每个愿望// for j = M .. m[i] ← 钱,倒序// for k = T .. t[i] ← 时间,倒序// f[j][k] = max(f[j][k], f[j-m][k-t] + 1)//// ⚠ 两个容量维**都**要倒序 —— 只要有一维写成正序,同一个愿望就能被实现好几次// (见 p1855Up.cpp,那时候它解的是「每个愿望可以许愿无限次」那道题)。
#include <bits/stdc++.h>using namespace std;
int f[205][205]; // f[j][k] = 花掉不超过 j 元、k 分钟,最多实现几个愿望
int main() { int n, M, T; if (scanf("%d %d %d", &n, &M, &T) != 3) return 0; for (int i = 0; i < n; i++) { int m, t; if (scanf("%d %d", &m, &t) != 2) return 0; for (int j = M; j >= m; j--) for (int k = T; k >= t; k--) f[j][k] = max(f[j][k], f[j - m][k - t] + 1); } printf("%d\n", f[M][T]); return 0;}点「运行 ▶」看结果
顶格 n = 100、M = T = 200 ⇒ 内层执行 |
4 000 000 次 |
f 表 201 × 201 个 int |
157 KB(限制 128 MB) |
⇒ 第 25 章正文第 7 步那句话在这道题上兑现了: 二维费用题的两个上限通常都开得很小(这里是 200 × 200)—— 看到题面里两个上限都是两三位数,出题人就是在提示你上二维费用。
5★ 错法(一):外层写成正序 —— 它精确地在解另一道题
// ✗ P1855 的错法(一):**外层**(钱)那一维写成了正序。//// 第 25 章正文第 5 步证过:二维费用里**方向的决定权只在外层**。// 外层一旦正序,`f[j-m][…]` 那一整行这一轮早就填完了 —— 同一个愿望可以被反复实现。//// ★ 而它不是「随便错」:它**精确地**解了「每个愿望可以实现无限次」那道题// (= p1855Full.cpp,300 轮逐组相等)。
#include <bits/stdc++.h>using namespace std;
int f[205][205];
int main() { int n, M, T; if (scanf("%d %d %d", &n, &M, &T) != 3) return 0; for (int i = 0; i < n; i++) { int m, t; if (scanf("%d %d", &m, &t) != 2) return 0; for (int j = m; j <= M; j++) // ✗ 外层正序 for (int k = T; k >= t; k--) // 内层仍然倒序,救不回来 f[j][k] = max(f[j][k], f[j - m][k - t] + 1); } printf("%d\n", f[M][T]); return 0;}点「运行 ▶」看结果
样例上它打出 10 —— 六个愿望却实现了十个,这一次样例挡住了。
| 300 轮:它 vs 二维费用的完全背包 | ★ 300 / 300 逐组相等 |
⇒ 第 23 章(01 写成正序 ≡ 完全背包)、第 24 章(完全写成倒序 ≡ 01 背包)、 第 25 章正文(二维费用外层正序 ≡ 二维费用的完全背包)—— 这是同一条恒等式的第四次现场。
6★★★ 错法(二):内层写成正序 —— 正文说「怎么写都一样」,那句话在这道题上还成立吗
第 25 章正文第 5 步专门证过一件事:
二维费用的方向决定权只在外层。外层倒序时,
f[j-m]那一整行这一轮根本没被碰过, 所以内层从左往右还是从右往左,读到的是同一个东西。
// P1855:外层倒序、**内层正序** —— 第 25 章正文说过「内层怎么写都一样」。//// ★★★ 这一页要问的是:那句话在这道题上还成立吗?//// 正文那条论证有一个没写出来的前提:`j - m` 得**真的是另一行**。// 而它成立的条件是 `m ≥ 1` —— 一旦某个愿望的**金钱费用是 0**,// `f[j-0][k-t]` 读的就是**同一行**,内层方向立刻变成决定性的:// 那个愿望会沿着时间轴被反复实现。//// ⚠ 而 P1855 的题面**根本没给 m、t 的下界**(只写了 `0 ≤ M ≤ 200`、`0 ≤ T ≤ 200`)。// ⇒ 页面上那张表:照「m, t ≥ 1」随机 300 轮抓 **0** 次(结构性的 0,不是概率低);// 生成器加一档「允许 m = 0」,当场 300 / 300。
#include <bits/stdc++.h>using namespace std;
int f[205][205];
int main() { int n, M, T; if (scanf("%d %d %d", &n, &M, &T) != 3) return 0; for (int i = 0; i < n; i++) { int m, t; if (scanf("%d %d", &m, &t) != 2) return 0; for (int j = M; j >= m; j--) // 外层倒序(对的) for (int k = t; k <= T; k++) // 内层正序 —— 只有 m = 0 时才咬人 f[j][k] = max(f[j][k], f[j - m][k - t] + 1); } printf("%d\n", f[M][T]); return 0;}点「运行 ▶」看结果
照题面随机跑 300 轮,它和正解 一次都没有不一致 —— 那句话看上去稳稳成立。
m = 0 的时候,f[j-0][k-t] 读的就是同一行,而内层正序会一路读到刚写过的格子
—— 那个愿望会沿着时间轴被反复实现。
⇒ 于是问题变成:题面允许 m = 0 吗?
翻回去看那两行:只写了 0 ≤ M ≤ 200、0 ≤ T ≤ 200,
关于 mᵢ、tᵢ 的取值范围,题面一个字都没有。
| 生成器 | 内层正序被抓 | 而「存在 m = 0 的愿望」的轮数 |
|---|---|---|
照顺手的写法随机(m, t ≥ 1) |
★ 精确的 0 | 0 |
| 允许费用为 0 | 51 / 300 | 132 / 300 |
★ 上面那个 0 不是概率低,是结构性的 —— 正文第 5 步那段论证就是它的证明, 加多少轮都不会变(第 12 章 P1226 那条「两种 0,造两档就能分开」)。 ⇒ 而分开之后结论很干净:这是「生成器缺一档」,不是「题面挡死」。
★ 顺带又量到一次「触发条件 ≠ 抓获数」:132 轮里只有 51 轮真被抓(差 2.6 倍)——
因为「存在 m = 0 的愿望」只是第一层,那个愿望还得真的落在最优解上、而且时间还有富余。
这条曲线本书已经量了十几次,从「一个不差」到「差 150 倍」都有。
7★★ 同一个旋钮,三个错法三条不同的曲线
| 比值(容量 ÷ 费用上限) | 1 | 2 | 4 | 8 | 16 |
|---|---|---|---|---|---|
| 贪心 | 1 | 9 | 13 | 28 | 23 |
| 合并费用 | 96 | 136 | 154 | ★ 174 | 120 |
| 外层正序 | 44 | 123 | 205 | 251 | ★ 286 |
外层正序是唯一一条单调的(44 → 286 一路涨); 另外两个都在第 8 档见顶之后回落(贪心 1 → 28 → 23,合并费用 96 → 174 → 120)。
⇒ 「顺手写的生成器够不够狠」这句话没有统一答案 —— 它得连着「你要抓哪个 bug」一起说。 ★ 而两头都差的原因不一样:比值太小时一个愿望都装不下(正解常常是 0), 比值太大时全都装得下(谁做都对)。
8★ 官方样例这一次不是「一测就死的过滤器」
本书从第 19 章起连着量到一条规律:官方样例挡住的往往是「每组都错」的错法, 放过的是「偶尔才错」的。这一页给它添了一个例外:
| 错法 | 样例挡住了吗 | 默认档 300 轮被抓 |
|---|---|---|
| 贪心 | ✗ 放过(也打 4) | 17 |
| 合并费用 | ✗ 放过(也打 4) | 148 |
| 内层正序 | ✗ 放过(也打 4) | 0(结构性) |
| 外层正序 | ★ 挡住(打 10) | 134 |
外层正序在默认档也只有 134 / 300,却被样例当场打死。 ⇒ 规律终究是规律,不是定律:样例挡不挡得住,只能一个一个试。
9度量程序、生成器和参照物
10一页纸
| 关键的一步 | 二维费用 01 背包的裸题,而且价值全是 1(只数件数)⇒ 多一维费用就多一层循环 |
| 规模 | 顶格 100 × 200 × 200 = 400 万次,f 表 157 KB —— 三十秒的算术 |
| ★ 读题的信号 | 两个上限都只有 200 ⇒ 出题人在提示你上二维费用(正文第 7 步那条) |
| 第一版:贪心 | 「便宜」没有主语 —— 两个上限各自独立。★ 抓获率第 8 档见顶后回落、错的幅度一路下降(两条曲线形状不同) |
| 第二版:合并费用 | 恒 ≥ 正解(300/300)—— 只会高估;默认档被抓 148 |
| 错法(一)外层正序 | ≡ 二维费用的完全背包(300/300 逐组相等);样例当场打死 |
| ★★★ 错法(二)内层正序 | 正文说「内层怎么写都一样」——⚠ 那句话的前提是 m ≥ 1;题面没给 mᵢ 的下界 |
| 那个「精确的 0」 | 照顺手的写法随机 0 / 300(结构性),允许 m = 0 当场 51 / 300 ⇒ 是生成器缺一档 |
| ★ 同一把旋钮 | 只有外层正序是单调的(44 → 286),贪心和合并费用都在第 8 档见顶后回落 |
| ⚠ 一致有两种 | 比值最小那一档,300 轮里 137 轮正解本身就是 0 —— 那些轮次验的是零 |