题单 · 习题解析

洛谷 P2925 [USACO08DEC] Hay For Sale S

★★★ 这道题只卡空间不卡时间:二维顶格 953 MB(限制 128),而时间余量 21 倍;★ 草稿被打回 —— char 比 int 只快 1.13 倍,真正快 23 倍的是 bitset

原题:洛谷 P2925出自 第 23 章 01 背包 的题单题面本地存档:2026-08-29
⚠ 先自己写一遍,再往下看

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

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

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

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

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

题目描述

农民 John 面临一个很可怕的事,因为防范力度不大所以他存储的所有稻草都被蟑螂吃光了, 他将面临没有稻草喂养奶牛的局面。在奶牛断粮之前,John 拉着他的马车到农民 Don 的农场中 买一些稻草给奶牛过冬。已知 John 的马车可以装的下 C (1 ≤ C ≤ 5 × 10⁴) 立方的稻草。

农民 Don 有 H (1 ≤ H ≤ 5 × 10³) 捆体积不同的稻草可供购买, 每一捆稻草有它自己的体积 Vᵢ (1 ≤ Vᵢ ≤ C)。面对这些稻草 John 认真的计算如何充分利用 马车的空间购买尽量多的稻草给他的奶牛过冬。

现在给定马车的最大容积 C 和每一捆稻草的体积 Vᵢ, John 如何在不超过马车最大容积的情况下买到最大体积的稻草?他不可以把一捆稻草分开来买。

输入格式

第一行两个整数,分别为 CH

第 2 到 H+1 行:每一行一个整数代表第 i 捆稻草的体积 Vᵢ

输出格式

一个整数,为 John 能买到的稻草的体积。

说明/提示

时限 1.5 秒,内存 128 MB

输入输出样例

输入

7 3
2
6
5

输出

7

马车容量 7,三捆稻草 2 / 6 / 5 ⇒ 拿 2 和 5 正好装满,输出 7。

★ 这一组样例当场挡住了贪心(贪心先拿 6,剩 1,装不下别的 ⇒ 输出 6)—— ⚠ 而同一个贪心在 P1049 的样例上是被放过的

★ 它和 P1049 是同一道题的两种问法
P1049 装箱问题 P2925(这道题)
问什么 剩余空间最小 装走的体积最大
关系 答案 = C − 剩余
n / H ≤ 30 5 × 10³
V / C ≤ 20000 5 × 10⁴
O(nV) 6 × 10⁵ 2.5 × 10⁸

转化那一步(把体积当价值 / 记布尔可达)一模一样,难的地方整个换了: 这道题卡的不是你会不会 DP,是你的表开不开得出来。

1第一版:贪心先拿大的(这一次样例不客气)

p2925Greedy.cpp错法一:先拿大的
// P2925 错法一:贪心 —— 体积从大到小,能装就装
//
// 和 [P1049](/sol/p1049/) 那个错法是同一个,错法也是同一个:
// **先把大的安顿好** ≠ **装得尽量满**。
//
// ★ 官方样例挡住了它:C = 7,三捆 2 / 6 / 5 ——
// 从大到小是 6、5、2 ⇒ 装 6(剩 1),5 和 2 都装不下 ⇒ 输出 6,而正解是 2 + 5 = 7。
// ⇒ 这一次样例不客气。⚠ 而 P1049 的样例**放过了**同一个错法 ——
// 「样例挡不挡得住」的主语是那一组样例长什么样。
#include <bits/stdc++.h>
using namespace std;
int main() {
int c, h;
if (!(cin >> c >> h)) return 0;
vector<int> v(h);
for (int& x : v) cin >> x;
sort(v.begin(), v.end(), greater<int>());
int left = c;
for (int x : v) if (x <= left) left -= x;
cout << c - left << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

样例上它输出 6 而正解是 7,一测就死

⚠ 但别把这条当成规律:同一个错法在 P1049 的样例上完好无损地活了下来。 ⇒ 「样例挡不挡得住」的主语永远是这一组样例长什么样

2★★★ 第二版:二维表 —— 答案是对的,可它开不出来

p2925Dp2.cpp第二版:二维(顶格 MLE)

转移和第 23 章那份 dp2.cpp 一个字不差,逻辑挑不出毛病。 把题面顶格代进去:

    (H + 1) × (C + 1) = 5001 × 50001 = 250 055 001 格
一格用什么装 顶格要多少内存 128 MB 够吗
int(4 字节) 953 MB
short(2 字节) 476 MB
char(1 字节) 238 MB
1 个 bit 29 MB ✓(但那已经不是二维表了)
★★★ 所以在这道题上,「压成一维」不是优化,是及格线

第 23 章第 ⑦ 步把「滚动数组」讲成一次空间上的改进。 这道题把那句话变成了硬约束:不压维,连编译出来的程序都跑不起来。

⚠ 而更值得注意的是时间那一头反而不紧张(见下一步)—— ⇒ 「这道题卡什么」要分开问:卡时间、卡空间,是两个不同的算术题。

3★ 正解:一维倒序(而时间余量有 21 倍)

p2925.cpp★ 这一版就能 AC
// P2925 [USACO08DEC] Hay For Sale S —— ★ 这一版就能 AC
//
// 题意:马车容量 C,H 捆稻草各有体积 Vi,不能拆开;问最多能装走多少体积。
//
// ★ 和 [P1049 装箱问题](/sol/p1049/) 是**同一道题的两种问法**:
// 那道问「剩余空间最小」,这道问「装走的体积最大」—— `答案 = C − 剩余`。
// 转化仍然是那一句:**只有体积没有价值 ⇒ 把体积当价值**(或者干脆记布尔可达)。
//
// ⚠⚠ 但这道题的数据范围和那道完全不是一个量级:
// C ≤ 5 × 10^4、H ≤ 5 × 10^3 ⇒ O(HC) = 2.5 × 10^8。
// ⇒ **二维表根本开不出来**(int 二维要 953 MB,而限制是 128 MB)。
// ★ 而时间反倒不紧张:顶格最坏形状本机 71 毫秒,时限 1500 —— 余量 21 倍。
// ⇒ 「压成一维」在这道题上**不是为了快,是为了开得出来**(解析页第 ③ 步)。
//
// ★ 这里记的是**布尔可达**(一个字节一格)而不是 int 表 —— ⚠ 而实测这一换**几乎不省时间**
// (71 vs 78 毫秒,1.1 倍;两版内层次数一次不差)。真正快 24 倍的是 bitset。
#include <bits/stdc++.h>
using namespace std;
const int MAXC = 50005;
char f[MAXC]; // f[j] = 「体积和恰好为 j」凑不凑得出来
int main() {
int c, h;
if (!(cin >> c >> h)) return 0;
f[0] = 1;
for (int i = 0; i < h; i++) {
int v; cin >> v;
for (int j = c; j >= v; j--) // 倒序:每捆只能买一次
if (f[j - v]) f[j] = 1;
}
for (int j = c; j >= 0; j--)
if (f[j]) { cout << j << "\n"; break; }
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

顶格的最坏形状C = 50000,5000 捆全是 3 —— 怎么装都装不满,一句也剪不掉):

内层循环次数 本机毫秒
布尔可达(char 数组) 249 990 000 约 70
时限 1500
余量 约 21 倍

★ 也就是说:这道题的 2.5 × 10⁸ 听着吓人,实际上时间是够的 —— 真正会把人挡在门外的是上一步那 953 MB。

4★★★ 只换数组类型能快多少 —— 这一段是草稿被实测打回来的

草稿里我写的是:「char 数组比 int 数组少读写四倍内存,应该快好几倍」。 实测不是这样。

p2925Int.cpp对照:一维但用 int 表
p2925Bitset.cpp更快:bitset 一句 f |= f << v
★★★ 同一个最坏形状,三种写法(本机 A:WSL2 / 8 核 / 7 GB,2026-08-29,独占)
写法 内层次数 毫秒 相对 char
布尔可达(char 249 990 000 ~70 1.0
体积当价值(int 249 990 000(一次不差) ~79 1.13 倍
bitsetf |= f << v —— 3 约 23 倍

前两行的次数逐个相同,秒表只差 13%。 ⇒ 「换成 char」在这道题上省的是空间,不是时间 (两个数组一个 50 KB、一个 200 KB,都还塞得进缓存)。

★ 真正快一个数量级的是 bitset,而它快的原因不是常数f |= f << v 一次推 64 个 bit,把那 2.5 × 10⁸ 次内层操作变成了约 4 × 10⁶ 次字操作 —— 它减少的是次数本身。

⇒ 这一页因此成了 P1074 那条反例侧: 那道题是「次数一样、秒表差 9.5 倍」,这道题是「次数一样、秒表也一样」。 「换一种存法值多少倍」只能量,不能推 —— 而且两个方向的例子都有。

5★★ 「凑满了就收工」那一刀:随机数据上 108 倍,最坏形状一分钱不省

p2925Break.cpp更快:凑满就停

答案的上限就是 C,所以 f[C] 一变成 true,剩下的稻草一捆都不用再算。

顶格 C = 50000H = 5000 不剪 剪了 倍数
体积随机 ∈ [1, 50000](很快就凑满) 124 296 667 1 153 173 107 倍
全是 3(永远凑不满 249 990 000 249 990 000 1.00 —— 一分钱不省
★ 一句读题换来的 107 倍,和一句「它救不了最坏情况」

这一刀在随机数据上值 100 多倍,在最坏形状上完全无效。 ⇒ 所以它不能当成「复杂度改进」写进结论里 —— 复杂度仍然是 O(HC)

★ 而这正是第 20 章 P5019 那条的另一面: 判断一个剪枝值不值,要看它在最坏形状上省下什么; 随机数据上的倍数很好看,但决定你会不会 TLE 的是最坏那一档。 (好在这道题最坏那一档本来就只要 70 毫秒。)

6★★ 错法二:一维正序 —— 而两个错法的抓获率又一次方向相反

p2925Up.cpp错法二:一维正序
// P2925 错法二:一维写成正序 —— 它精确地在解完全背包
//
// 正序 ⇒ 同一捆稻草可以被**买好几次**。于是它算出来的
// **恒等于「每捆稻草有无限多份」那道题的答案**(解析页第 ④ 步量成 300 组逐组相等)。
//
// ★ 在这道题上它有一个特别扎眼的后果:只要有一捆稻草的体积能整除某段空隙,
// 它就能把马车塞满 —— 而**只要有一捆体积为 1 的,它必定输出 C**。
//
// ⚠ 官方样例挡住了它:C = 7,2 / 6 / 5 ⇒ 正序能凑出 2+5 = 7 也能凑出别的,
// 输出仍是 7,和正解一样 —— ★ 不,样例**放过了它**(见解析页第 ④ 步那张表)。
#include <bits/stdc++.h>
using namespace std;
const int MAXC = 50005;
char f[MAXC];
int main() {
int c, h;
if (!(cin >> c >> h)) return 0;
f[0] = 1;
for (int i = 0; i < h; i++) {
int v; cin >> v;
for (int j = v; j <= c; j++) // ⚠ 正序
if (f[j - v]) f[j] = 1;
}
for (int j = c; j >= 0; j--)
if (f[j]) { cout << j << "\n"; break; }
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
随机 300 组:正序的答案 vs 显式写的完全背包 300 / 300 逐组相等

⇒ 它精确地解了「每捆稻草有无限多份」那道题。 ⚠ 官方样例放过了它2 + 5 = 7 用不着重复买)。

★★ 同一个旋钮,贪心和正序又一次走向相反 —— 这是这个现象的第二次

C ≤ 60H ≤ 12 固定,只拧体积上限,每档 300 轮:

体积上限 3 10 20 60
贪心「先拿大的」被抓 0 30 65 76
「正序 = 完全背包」被抓 225 125 83 61

P1049 那张表同一个形状(那一页是 0/300 → 124/—)。 ⇒ 「为一个 bug 精心造的档位正是另一个 bug 的盲区」这件事,在同一类题上重复出现了。

★ 而这一次那个 0 是概率低,不是结构性的:加深到两万轮,贪心被抓 61 次(0.31%)。 ⚠ 和 P1049 体积 ≤ 3 那一档正好相反 —— 那一档是一句乘法算出来的精确 0。 ⇒ 看到 0 就去加深轮数:两种 0 长得一模一样,而救法完全不同。

7度量程序和生成器

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

8一页纸

关键的一步 P1049 同一句:只有体积没有价值 ⇒ 体积当价值(或记布尔可达)
哪一版能 AC p2925.cpp —— 一维倒序 char 表,顶格最坏 约 70 毫秒(时限 1500)
★★★ 真正的关卡 二维表顶格 953 MB(限制 128 MB)⇒ 压成一维不是优化,是及格线
⚠ 卡时间还是卡空间 这道题只卡空间:时间余量约 21 倍 —— 两个算术题要分开算
★★★ 被打回的草稿 charint 快好几倍」是错的:次数一次不差,秒表只差 1.13 倍
真正快的 bitsetf |= f << v 一次推 64 bit ⇒ 约 23 倍(它减少的是次数本身)
★★ 提前退出 随机数据 107 倍,最坏形状 1.00 倍 ⇒ 不能当复杂度改进写
错法一 贪心先拿大的 —— ★ 样例当场挡住(而 P1049 的样例放过了同一个错法)
错法二 一维正序 ⇒ 恒等于完全背包(300/300);样例放过
★★ 生成器 体积上限这个旋钮把两个 bug 推向相反方向(3 ⇒ 贪心 0 / 正序 225;60 ⇒ 76 / 61)
⚠ 那个 0 概率低(两万轮 61 次),不是结构性的 —— 和 P1049 那个精确 0 正好相反