0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1507,日期见页头。两边不一致时信原站。
题目背景
NASA(美国航空航天局)因为航天飞机的隔热瓦等其他安全技术问题一直大伤脑筋, 因此在各方压力下终止了航天飞机的历史,但是此类事情会不会在以后发生,谁也无法保证。 所以,在遇到这类航天问题时,也许只能让航天员出仓维修。 但是过多的维修会消耗航天员大量的能量, 因此 NASA 便想设计一种食品方案,使体积和承重有限的条件下多装载一些高卡路里的食物。
题目描述
航天飞机的体积有限,当然如果载过重的物品,燃料会浪费很多钱, 每件食品都有各自的体积、质量以及所含卡路里。 在告诉你体积和质量的最大值的情况下,请输出能达到的食品方案所含卡路里的最大值, 当然每个食品只能使用一次。
输入格式
第一行 2 个整数,分别代表体积最大值 H 和质量最大值 T。
第二行 1 个整数代表食品总数 n。
接下来 n 行每行 3 个数 体积 hᵢ,质量 tᵢ,所含卡路里 kᵢ。
输出格式
一个数,表示所能达到的最大卡路里(int 范围内)
说明/提示
对于 100% 的数据,H, T, hᵢ, tᵢ ≤ 400,n ≤ 50,kᵢ ≤ 500。
输入输出样例
输入
320 350 4 160 40 120 80 110 240 220 70 310 40 400 220
输出
550
体积上限 320、质量上限 350,四件食品。取第 2 件 (80, 110, 240) 和第 3 件 (220, 70, 310):
体积 300、质量 180,两样都没超 ⇒ 550。
★ 第 4 件的质量是 400,比上限还大 —— 它一件都装不下。
⚠ 这一组样例三个错法只挡住了一个(忘了质量那一维,打出 580)—— 性价比贪心和「两列读反」都照样打出 550。
1★ 先认出来:它和 P1855 是同一道题
P1855 是「每个愿望花钱和时间,最多实现几个」; 这道题是「每件食品占体积和质量,卡路里最多能拿多少」。
两个上限、每件两种代价、每件最多拿一次 —— 一模一样。唯一的差别只有一处:
| P1855 | 本题 | |
|---|---|---|
| 每件的价值 | 恒等于 1(只数件数) | kᵢ(卡路里) |
| 状态转移 | f[j][k] = max(f[j][k], f[j-m][k-t] + 1) |
f[j][l] = max(f[j][l], f[j-h][l-t] + kᵢ) |
⇒ 把 + 1 换成 + kᵢ,别的一个字都不用改。
check:viz 把这句话钉成了断言:把本题的卡路里全设成 1,
两道题的两份程序在 300 组数据上逐字节相同。
2第一版:性价比贪心 —— 「每单位代价换多少卡路里」
价值不再恒等于 1 之后,最顺的第一反应从「先挑便宜的」变成了「先挑划算的」:
按 k / (h + t) 从大到小排,能装就装。
// ✗ P1507 的第一版:性价比贪心。//// 「每单位代价能换多少卡路里」听起来天经地义 —— 这一版按 `k / (h + t)` 从大到小排,// 能装就装。它是[第 20 章 P1048](/sol/p1048/) 那个上当的**二维版本**。//// ⚠ 两处一起错:// ① 分母把两种代价加起来 —— 而这两个上限是各自独立的;// ② 就算只有一维,01 背包的性价比贪心本来就不对(不能切)。//// ★ 页面上量过:它的形状和 P1048 一模一样 ——**不是经常错得离谱,是偶尔错一点点**。
#include <bits/stdc++.h>using namespace std;
struct Item { int h, t, k; };
int main() { int H, T, n; if (scanf("%d %d", &H, &T) != 2) return 0; if (scanf("%d", &n) != 1) return 0; vector<Item> a(n); for (int i = 0; i < n; i++) if (scanf("%d %d %d", &a[i].h, &a[i].t, &a[i].k) != 3) return 0;
sort(a.begin(), a.end(), [](const Item& x, const Item& y) { // 交叉相乘,避开浮点(分母可能是 0:体积和质量都为 0 的食品) return (long long)x.k * (y.h + y.t) > (long long)y.k * (x.h + x.t); });
long long sum = 0; for (auto& it : a) if (it.h <= H && it.t <= T) { H -= it.h; T -= it.t; sum += it.k; } printf("%lld\n", sum); return 0;}点「运行 ▶」看结果
样例上它打出 550,和正解一样。
① 分母把两种代价加起来了 —— 而这两个上限是各自独立的(钱不够不能拿时间来垫); ② 就算只有一维,01 背包的性价比贪心本来就不对 —— 那是第 19 章 P2240 那种「能切开」的题才成立的。
| 比值(容量 ÷ 费用上限) | 1 | 2 | 4 | 8 | 16 |
|---|---|---|---|---|---|
| 性价比贪心被抓 | 35 | 55 | ★ 68 | 64 | 46 |
| 错的时候平均少 | 18.16% | 13.98% | 12.31% | 9.43% | ★ 8.64% |
| ⚠ 而正解本身就是 0 的轮数 | 101 | 45 | 17 | 9 | 3 |
★ 和第 20 章 P1048 一模一样的两种形状:
抓获率是单峰的(第 4 档最高),而错的幅度一路往下。
⇒ 「多久错一次」和「错的时候差多少」是两件事 —— 这已经是本书第三次量到单峰
(第 13 章 P1596 沿密度、P1048 沿 T、这里沿比值)。
⚠ 第三行照旧要看:比值 1 那一档 300 轮里 101 轮正解本身就是 0,那些轮次「一致」验的是零。
3第二版:只盯着体积,把质量那一维忘了
题面里两个上限并排写着,而代码里只写了一个 for j = H .. h[i]。
// ✗ P1507 的第二版:只盯着体积,把质量那一维忘了。//// 这是「二维费用」题最典型的漏法 —— 题面里两个上限并排写着,// 而代码里只有一个 `for j = H .. h[i]`。//// ★ 它有一条能写下来的性质:**答案恒 ≥ 正解**(少了一堵墙,可行集只会变大)。// ⇒ 它交上去是「答案偏大」的 WA,而且**它给出的那个方案往往是超重的**。
#include <bits/stdc++.h>using namespace std;
int f[405];
int main() { int H, T, n; if (scanf("%d %d", &H, &T) != 2) return 0; if (scanf("%d", &n) != 1) return 0; (void)T; // ✗ 质量读进来了,然后再也没用过 for (int i = 0; i < n; i++) { int h, t, k; if (scanf("%d %d %d", &h, &t, &k) != 3) return 0; (void)t; for (int j = H; j >= h; j--) f[j] = max(f[j], f[j - h] + k); } printf("%d\n", f[H]); return 0;}点「运行 ▶」看结果
样例上它打出 580 —— 这一次样例挡住了。
| 300 轮里「它的答案 ≥ 正解」 | ★ 300 / 300 |
| 默认档里它被抓 | 162 / 300 |
而它选出的那一组真的超重(质量 > T) |
★ 162 / 300 |
★ 后两行一个不差:它被抓 ⟺ 它给出的方案超重。 少了一堵墙,可行集只会变大 ⇒ 它只会高估;而一旦高估,那个方案必然是「装不进去」的。 ⇒ 又一次「说清楚一个 bug 算了什么,比说它错了有用得多」。
4★ 正解:多一维费用,就多一层循环
// P1507 NASA的食物计划 —— ★ 这一版就能 AC。//// 和同一章的 [P1855](/sol/p1855/) 是**同一道题**,只换了一处:// 那道题「每个愿望都值 1 分」(只数件数),这道题每件食品有自己的卡路里。// ⇒ 状态转移里那个 `+ 1` 换成 `+ k[i]`,别的一个字都不用改。//// for 每件食品 i// for j = H .. h[i] ← 体积,倒序// for l = T .. t[i] ← 质量,倒序// f[j][l] = max(f[j][l], f[j-h][l-t] + k)//// ⚠ 输入格式是三段:第一行 H T,第二行 n,再往下每行三个数 —— 顺序是**体积、质量、卡路里**。
#include <bits/stdc++.h>using namespace std;
int f[405][405]; // f[j][l] = 体积不超过 j、质量不超过 l 时的最大卡路里
int main() { int H, T, n; if (scanf("%d %d", &H, &T) != 2) return 0; if (scanf("%d", &n) != 1) return 0; for (int i = 0; i < n; i++) { int h, t, k; if (scanf("%d %d %d", &h, &t, &k) != 3) return 0; for (int j = H; j >= h; j--) for (int l = T; l >= t; l--) f[j][l] = max(f[j][l], f[j - h][l - t] + k); } printf("%d\n", f[H][T]); return 0;}点「运行 ▶」看结果
顶格 n = 50、H = T = 400 ⇒ 内层执行 |
8 000 000 次 |
滚掉第一维之后 f 表 401 × 401 个 int |
628 KB |
⚠ 不滚动的三维表 51 × 401 × 401 |
31 MB(限制 128 MB) |
⇒ ★ 注意最后一行:三维也能过。省掉那一维是「没必要留」,不是「留了就挂」—— 这和 P1853 那道「不除 1000 直接 MLE」 不是同一件事, 两个都要自己乘一遍才知道站在哪一边。
5★★ 第三版:两列读反了 —— 而这个错法配了一个「算得出来的 0」
输入格式那三行读起来很顺,可每行三个数的顺序是要一个字一个字对的。 这一版把每件食品的前两列读成了「质量、体积」,而两个上限没跟着换:
// ✗ P1507 的第三版:把「体积」和「质量」这两列读反了。//// 输入格式那三行读起来很顺,可**每行三个数的顺序**是要一个字一个字对的:// 第一行 H T ← 体积上限、质量上限// 每件食品 h t k ← 体积、质量、卡路里// 这一版把每件食品的前两个数读成了「质量、体积」,而两个上限没跟着换。//// ★ 页面上量过:它**不是一定错** —— 只有当两个上限(或两列费用)真的不对称时才会现形。
#include <bits/stdc++.h>using namespace std;
int f[405][405];
int main() { int H, T, n; if (scanf("%d %d", &H, &T) != 2) return 0; if (scanf("%d", &n) != 1) return 0; for (int i = 0; i < n; i++) { int h, t, k; if (scanf("%d %d %d", &t, &h, &k) != 3) return 0; // ✗ 前两列读反了 for (int j = H; j >= h; j--) for (int l = T; l >= t; l--) f[j][l] = max(f[j][l], f[j - h][l - t] + k); } printf("%d\n", f[H][T]); return 0;}点「运行 ▶」看结果
样例上它照样打出 550(放过了)。
| 生成器 | 读反被抓 | 而「两维不对称」的轮数 |
|---|---|---|
默认档(H、T 各自随机,每件两维各自随机) |
207 / 300 | 299 / 300 |
★ 两维对称档(H = T 且每件 h = t) |
★ 精确的 0 | 0 |
★ 下面那个 0 不用跑就知道:两维完全对称时,「读反」是恒等变换。 ⇒ 所以它同时是一次自检 —— P2240 / P1094 立下的那条规矩: 报「精确的 0」之前,先证明那段代码是活的。这里反过来用: 那一档的 0 是算出来的,它和默认档的 207 用的是同一段对拍代码。
★ 顺带又量到一次「触发条件 ≠ 抓获数」:不对称有 299 轮,真被抓 207 轮(差 1.44 倍)—— 两维不对称只是第一层,还得那个不对称真的卡住某件食品。
6★ 题面那句「int 范围内」是噪声 —— 连 16 位都够
输出格式那一行写着「所能达到的最大卡路里(int 范围内)」。
按第 12 章那条判据称一称它是情报 / 命门 / 噪声里的哪一种 ——
这一次连数据都不用造,乘一遍就行:
答案 ≤ n × max(kᵢ) = 50 × 500 = 25 000
int 的上限 = 2 147 483 647 ⇒ 余量 85 899 倍
16 位有符号的上限 = 32 767 ⇒ ★ 连 short 都装得下
⇒ 它是噪声:这道题连「要不要 long long」这个问题都不存在。
⚠ 而同一章的 P1064 里那句「答案不超过 2 × 10⁵」就不是噪声 ——
每一句「保证」都得自己乘一遍,松紧差得远(P1164 恰好够、P1060 松了 666 倍)。
7度量程序、生成器和参照物
8一页纸
| 关键的一步 | 和 P1855 是同一道题:+ 1 换成 + kᵢ —— 卡路里全设成 1 时两份程序 300 组逐字节相同 |
| 规模 | 顶格 50 × 400 × 400 = 800 万次;f 表 628 KB,⚠ 不滚动的三维表 31 MB(限制 128 ⇒ 也能过) |
| 第一版:性价比贪心 | 一次错两处(分母合并了两种代价 / 01 背包本来就不能贪)。★ 抓获率单峰、错的幅度单调下降 |
| 第二版:忘了质量 | 恒 ≥ 正解(300/300);★ 被抓 162 ⟺ 方案超重 162,一个不差 |
| ★★ 第三版:两列读反 | 默认档 207/300;★ 两维对称那一档是算得出来的精确 0 —— 它同时就是那段对拍代码的自检 |
| ★ 那句「int 范围内」 | 噪声:答案上界 50 × 500 = 25 000,连 16 位都够(余量 85 899 倍) |
| ⚠ 一致有两种 | 比值最小那一档,300 轮里 101 轮正解本身就是 0 |