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 如何在不超过马车最大容积的情况下买到最大体积的稻草?他不可以把一捆稻草分开来买。
输入格式
第一行两个整数,分别为 C 和 H。
第 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 装箱问题 | P2925(这道题) | |
|---|---|---|
| 问什么 | 剩余空间最小 | 装走的体积最大 |
| 关系 | 答案 = C − 剩余 |
|
n / H |
≤ 30 | ≤ 5 × 10³ |
V / C |
≤ 20000 | ≤ 5 × 10⁴ |
O(nV) |
6 × 10⁵ | ★ 2.5 × 10⁸ |
⇒ 转化那一步(把体积当价值 / 记布尔可达)一模一样,难的地方整个换了: 这道题卡的不是你会不会 DP,是你的表开不开得出来。
1第一版:贪心先拿大的(这一次样例不客气)
// 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;}点「运行 ▶」看结果
样例上它输出 6 而正解是 7,一测就死。
⚠ 但别把这条当成规律:同一个错法在 P1049 的样例上完好无损地活了下来。 ⇒ 「样例挡不挡得住」的主语永远是这一组样例长什么样。
2★★★ 第二版:二维表 —— 答案是对的,可它开不出来
转移和第 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 [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;}点「运行 ▶」看结果
顶格的最坏形状(C = 50000,5000 捆全是 3 —— 怎么装都装不满,一句也剪不掉):
| 内层循环次数 | 本机毫秒 | |
|---|---|---|
布尔可达(char 数组) |
249 990 000 | ★ 约 70 |
| 时限 | 1500 | |
| 余量 | ★ 约 21 倍 |
★ 也就是说:这道题的 2.5 × 10⁸ 听着吓人,实际上时间是够的 ——
真正会把人挡在门外的是上一步那 953 MB。
4★★★ 只换数组类型能快多少 —— 这一段是草稿被实测打回来的
草稿里我写的是:「char 数组比 int 数组少读写四倍内存,应该快好几倍」。
实测不是这样。
| 写法 | 内层次数 | 毫秒 | 相对 char |
|---|---|---|---|
布尔可达(char) |
249 990 000 | ~70 | 1.0 |
体积当价值(int) |
★ 249 990 000(一次不差) | ~79 | 1.13 倍 |
bitset(f |= 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 倍,最坏形状一分钱不省
答案的上限就是 C,所以 f[C] 一变成 true,剩下的稻草一捆都不用再算。
顶格 C = 50000、H = 5000 |
不剪 | 剪了 | 倍数 |
|---|---|---|---|
| 体积随机 ∈ [1, 50000](很快就凑满) | 124 296 667 | 1 153 173 | ★ 107 倍 |
| 全是 3(永远凑不满) | 249 990 000 | 249 990 000 | ★ 1.00 —— 一分钱不省 |
这一刀在随机数据上值 100 多倍,在最坏形状上完全无效。
⇒ 所以它不能当成「复杂度改进」写进结论里 —— 复杂度仍然是 O(HC)。
★ 而这正是第 20 章 P5019 那条的另一面: 判断一个剪枝值不值,要看它在最坏形状上省下什么; 随机数据上的倍数很好看,但决定你会不会 TLE 的是最坏那一档。 (好在这道题最坏那一档本来就只要 70 毫秒。)
6★★ 错法二:一维正序 —— 而两个错法的抓获率又一次方向相反
// 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;}点「运行 ▶」看结果
| 随机 300 组:正序的答案 vs 显式写的完全背包 | ★ 300 / 300 逐组相等 |
⇒ 它精确地解了「每捆稻草有无限多份」那道题。
⚠ 官方样例放过了它(2 + 5 = 7 用不着重复买)。
C ≤ 60、H ≤ 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度量程序和生成器
8一页纸
| 关键的一步 | 和 P1049 同一句:只有体积没有价值 ⇒ 体积当价值(或记布尔可达) |
| 哪一版能 AC | p2925.cpp —— 一维倒序 char 表,顶格最坏 约 70 毫秒(时限 1500) |
| ★★★ 真正的关卡 | 二维表顶格 953 MB(限制 128 MB)⇒ 压成一维不是优化,是及格线 |
| ⚠ 卡时间还是卡空间 | 这道题只卡空间:时间余量约 21 倍 —— 两个算术题要分开算 |
| ★★★ 被打回的草稿 | 「char 比 int 快好几倍」是错的:次数一次不差,秒表只差 1.13 倍 |
| 真正快的 | bitset:f |= f << v 一次推 64 bit ⇒ 约 23 倍(它减少的是次数本身) |
| ★★ 提前退出 | 随机数据 107 倍,最坏形状 1.00 倍 ⇒ 不能当复杂度改进写 |
| 错法一 | 贪心先拿大的 —— ★ 样例当场挡住(而 P1049 的样例放过了同一个错法) |
| 错法二 | 一维正序 ⇒ 恒等于完全背包(300/300);样例放过 |
| ★★ 生成器 | 体积上限这个旋钮把两个 bug 推向相反方向(3 ⇒ 贪心 0 / 正序 225;60 ⇒ 76 / 61) |
| ⚠ 那个 0 | 是概率低(两万轮 61 次),不是结构性的 —— 和 P1049 那个精确 0 正好相反 |