题单 · 习题解析

洛谷 P1507 NASA的食物计划

★★ 和 P1855 是同一道题(卡路里全设成 1 就逐字节相同);★ 「忘了质量那一维」被抓 162 轮 ⟺ 它给的方案超重 162 轮,一个不差

原题:洛谷 P1507出自 第 25 章 二维费用与分组背包 的题单题面本地存档:2026-08-29
⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目背景

NASA(美国航空航天局)因为航天飞机的隔热瓦等其他安全技术问题一直大伤脑筋, 因此在各方压力下终止了航天飞机的历史,但是此类事情会不会在以后发生,谁也无法保证。 所以,在遇到这类航天问题时,也许只能让航天员出仓维修。 但是过多的维修会消耗航天员大量的能量, 因此 NASA 便想设计一种食品方案,使体积和承重有限的条件下多装载一些高卡路里的食物。

题目描述

航天飞机的体积有限,当然如果载过重的物品,燃料会浪费很多钱, 每件食品都有各自的体积、质量以及所含卡路里。 在告诉你体积和质量的最大值的情况下,请输出能达到的食品方案所含卡路里的最大值, 当然每个食品只能使用一次。

输入格式

第一行 2 个整数,分别代表体积最大值 H 和质量最大值 T

第二行 1 个整数代表食品总数 n

接下来 n 行每行 3 个数 体积 hᵢ,质量 tᵢ,所含卡路里 kᵢ

输出格式

一个数,表示所能达到的最大卡路里(int 范围内)

说明/提示

对于 100% 的数据,H, T, hᵢ, tᵢ ≤ 400n ≤ 50kᵢ ≤ 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) 从大到小排,能装就装。

p1507Greedy.cpp✗ 第一版:性价比贪心
// ✗ 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

样例上它打出 550,和正解一样。

⚠ 它一次错两处

① 分母把两种代价加起来了 —— 而这两个上限是各自独立的(钱不够不能拿时间来垫); ② 就算只有一维,01 背包的性价比贪心本来就不对 —— 那是第 19 章 P2240 那种「能切开」的题才成立的。

★★ 量一遍:这是 P1048 那对曲线的二维版(`n ≤ 14`,单件费用 ≤ 12,各 300 轮)
比值(容量 ÷ 费用上限) 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]

p1507NoMass.cpp✗ 第二版:忘了质量那一维
// ✗ 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

样例上它打出 580 —— 这一次样例挡住了

★ 而它「算了什么」能说死,两条数字一个不差
300 轮里「它的答案 ≥ 正解」 300 / 300
默认档里它被抓 162 / 300
而它选出的那一组真的超重(质量 > T 162 / 300

★ 后两行一个不差它被抓 ⟺ 它给出的方案超重。 少了一堵墙,可行集只会变大 ⇒ 它只会高估;而一旦高估,那个方案必然是「装不进去」的。 ⇒ 又一次「说清楚一个 bug 算了什么,比说它错了有用得多」。

4★ 正解:多一维费用,就多一层循环

p1507.cpp★ 这一版就能 AC
// 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
★ 能不能这么做,是一道三十秒的算术题
顶格 n = 50H = T = 400 ⇒ 内层执行 8 000 000
滚掉第一维之后 f401 × 401int 628 KB
⚠ 不滚动的三维表 51 × 401 × 401 31 MB(限制 128 MB)

⇒ ★ 注意最后一行:三维也能过。省掉那一维是「没必要留」,不是「留了就挂」—— 这和 P1853 那道「不除 1000 直接 MLE」 不是同一件事, 两个都要自己乘一遍才知道站在哪一边。

p1507Cube.cpp(不滚动的三维写法,它是对的)对照:把第三维原样留着

5★★ 第三版:两列读反了 —— 而这个错法配了一个「算得出来的 0」

输入格式那三行读起来很顺,可每行三个数的顺序是要一个字一个字对的。 这一版把每件食品的前两列读成了「质量、体积」,而两个上限没跟着换:

p1507Swap.cpp✗ 第三版:体积和质量读反
// ✗ 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;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

样例上它照样打出 550(放过了)。

★★ 它的触发条件是「两维不对称」,而对称那一档是精确的 0
生成器 读反被抓 而「两维不对称」的轮数
默认档(HT 各自随机,每件两维各自随机) 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度量程序、生成器和参照物

p1507Count.cpp度量程序(本页所有数字都出自它)
p1507Gen.cpp数据生成器
p1507Brute.cpp参照物:2ⁿ 枚举子集(300 轮不一致 0 轮)

8一页纸

关键的一步 P1855 是同一道题+ 1 换成 + kᵢ —— 卡路里全设成 1 时两份程序 300 组逐字节相同
规模 顶格 50 × 400 × 400 = 800 万次;f628 KB,⚠ 不滚动的三维表 31 MB(限制 128 ⇒ 也能过
第一版:性价比贪心 一次错两处(分母合并了两种代价 / 01 背包本来就不能贪)。★ 抓获率单峰、错的幅度单调下降
第二版:忘了质量 恒 ≥ 正解(300/300);★ 被抓 162 ⟺ 方案超重 162,一个不差
★★ 第三版:两列读反 默认档 207/300;★ 两维对称那一档是算得出来的精确 0 —— 它同时就是那段对拍代码的自检
★ 那句「int 范围内」 噪声:答案上界 50 × 500 = 25 000,连 16 位都够(余量 85 899 倍)
⚠ 一致有两种 比值最小那一档,300 轮里 101 轮正解本身就是 0