0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P1049,日期见页头。两边不一致时信原站。
题目描述
有一个箱子容量为 V,同时有 n 个物品,每个物品有一个体积。
现在从 n 个物品中,任取若干个装入箱内(也可以不取),使箱子的剩余空间最小。输出这个最小值。
输入格式
第一行共一个整数 V,表示箱子容量。
第二行共一个整数 n,表示物品总数。
接下来 n 行,每行有一个正整数,表示第 i 个物品的体积。
输出格式
共一行一个整数,表示箱子最小剩余空间。
数据规模与约定
对于 100% 的数据,满足 0 < n ≤ 30,1 ≤ V ≤ 20000。
【题目来源】 NOIP 2001 普及组第四题
输入输出样例
输入
24 6 8 3 12 7 9 7
输出
0
箱子容量 24,六件物品 8 3 12 7 9 7 —— 有办法正好装满(8 + 9 + 7 = 24),所以剩余 0。
⚠ 这一组样例放过了本页三个错法里的两个(第 ① 步和第 ⑤ 步各一个), 只挡住了最显眼的那一个(第 ⑥ 步)。
题单里给它写的提示是「只有体积没有价值 —— 把体积同时当价值就行」。 这一步转化是背包题的第一道门槛,值得说透:
01 背包的模板要两样东西 —— 每件物品的体积和价值。 而这道题只给了体积。缺的那一样从哪儿来?
要「剩余空间最小」,就是要「装进去的体积之和最大」。 而「装进去的体积之和」正好就是「选中物品的体积和」。 ⇒ 于是
价值 = 体积,跑一遍标准 01 背包,答案是V - f[V]。
⚠ 而这一页真正想讲的不是这一步(它两行就说完了),是后面那三件事: 样例放过了什么、暴力凭什么看着能过、以及同一个旋钮怎么把两个 bug 推向相反的方向。
1第一版:先拿大的(而官方样例放过了它)
大多数人的第一反应是贪心:体积从大到小排序,能塞就塞。
// P1049 错法一:贪心 —— 体积从大到小,能塞就塞//// 这是大多数人的第一反应,而且**官方样例放过了它**:// V = 24,物品 8 3 12 7 9 7 ⇒ 从大到小是 12 9 8 7 7 3,// 塞 12(剩 12)、塞 9(剩 3)、8 塞不下、7 塞不下、7 塞不下、塞 3(剩 0)⇒ 输出 0,和正解一样。//// ⚠ 而它是错的,最小的反例只要三件物品:// V = 10,物品 6 5 5 ⇒ 贪心先拿 6,剩 4,两个 5 都塞不下 ⇒ 剩 4// 正解 5 + 5 = 10 ⇒ 剩 0//// ★ 「先拿大的」错在哪:它把「装得尽量满」偷换成了「先把大的安顿好」,// 而背包问题里**一件物品该不该拿,取决于剩下的空间还能不能被别的物品配平** ——// 这正是第 20 章那一整章在讲的事(贪心要证,不能猜)。
#include <bits/stdc++.h>using namespace std;
int main() { int V, n; if (!(cin >> V >> n)) return 0; vector<int> v(n); for (int& x : v) cin >> x;
sort(v.begin(), v.end(), greater<int>()); int left = V; for (int x : v) if (x <= left) left -= x; // 能塞就塞
cout << left << "\n"; return 0;}点「运行 ▶」看结果
它在官方样例上是对的:12 → 9 → 3 正好凑满 24,输出 0,和正解一模一样。
而最小的反例只要三件物品:
V = 10,物品 6 5 5
贪心 : 先拿 6,剩 4 —— 两个 5 都塞不下 -> 剩 4
正解 : 5 + 5 = 10 -> 剩 0
它把「装得尽量满」偷换成了「先把大的安顿好」。 而背包问题里,一件物品该不该拿,取决于剩下的空间还能不能被别的物品配平 —— 这正是第 20 章那一整章在讲的事:贪心要证,不能猜。
2★★★ 第二版:暴力 + 一句剪枝 —— 它看着能过,是因为你的数据太软
n ≤ 30 ⇒ 子集有 2³⁰ ≈ 1.07 × 10⁹ 个,第一眼就该判它过不去。
但只要加一句剪枝(装不下就不往下递归),随手造几组数据一测,它快得像正解:
同一个 n,只换体积的形状(V = 20000,形状 A 的数字是 20 组的平均):
n |
A:体积 ∈ [1, 20000] | B:体积全 ≤ 3 | B / A |
|---|---|---|---|
| 20 | 4 585 | 2 097 151 | 457 倍 |
| 22 | 11 436 | 8 388 607 | 734 倍 |
| 24 | 21 540 | 33 554 431 | 1 558 倍 |
★ 形状 B 那一列不是「大概」,是精确的等式:体积全 ≤ 3 时怎么装都装不满,
那句剪枝一刀也剪不动,于是递归次数恰好是整棵二叉树的结点数 2ⁿ⁺¹ − 1
(2 097 151 = 2²¹ − 1,8 388 607 = 2²³ − 1,33 554 431 = 2²⁵ − 1,三个全中)。
⇒ 题面顶格 n = 30 时它是 2 147 483 647 次 —— 这是算出来的,不是估的。
⚠⚠ 而形状 A 之所以快,是因为箱子塞两三件就满了,那棵树一进门就被砍光。
这是第 20 章 P5019 那条结论在搜索题上的又一次现场:
官方样例是个「一测就死」的过滤器 —— 但它筛的是「答案错」, 对「答案对但跑不完」完全无能为力。对拍也一样。
这一版的答案永远是对的(本页第 ④ 步的对拍里它一次都没错), 错的只是「你测的那几组数据恰好让它跑得飞快」。 ⇒ 判它死刑要靠数次数 + 造对形状,而不是靠跑一遍看表。
3★ 正解:把体积当价值(这一版就能 AC)
// P1049 [NOIP 2001 普及组] 装箱问题 —— ★ 这一版就能 AC//// 题意:箱子容量 V,n 个物品各有一个体积,任取若干件装进去(也可以不取),// 问箱子**剩余空间**最小是多少。//// ★★ 这道题的第一道门槛不在算法,在**读题**:它给的物品**只有体积,没有价值**。// 而 01 背包的模板要「体积」和「价值」两样东西。缺的那样从哪儿来?//// **把体积同时当成价值。**//// 一句话的道理:要「剩余空间最小」,就是要「装进去的体积之和最大」——// 而「装进去的体积之和」正好就是「选中物品的体积和」。// ⇒ 于是 w[i] = v[i] = 体积,跑一遍标准 01 背包,答案是 `V - f[V]`。//// ⚠ 别忘了最后那一下减法:f[V] 是**装进去多少**,题目问的是**剩下多少**。// (这一下漏掉的话官方样例当场就会打出 24 而不是 0 —— 见解析页第 ⑥ 步。)//// 复杂度 O(nV) = 30 × 20000 = 6 × 10^5 —— 而 2^n 是 1.07 × 10^9,差 1789 倍。
#include <bits/stdc++.h>using namespace std;
int main() { int V, n; if (!(cin >> V >> n)) return 0; vector<int> v(n); for (int& x : v) cin >> x;
vector<int> f(V + 1, 0); // f[j] = 容量恰好 j 时,最多能装进去的体积 for (int i = 0; i < n; i++) for (int j = V; j >= v[i]; j--) // ★ 倒序:每件最多用一次(第 23 章第 ⑧ 步) f[j] = max(f[j], f[j - v[i]] + v[i]);
cout << V - f[V] << "\n"; // ⚠ 问的是剩余空间 return 0;}点「运行 ▶」看结果
复杂度 O(nV),顶格 30 × 20000 = 6 × 10⁵ —— 而 2³⁰ 是 1.07 × 10⁹,
差 1789 倍。这个比值就是「为什么非 DP 不可」的全部答案。
4另外两条路:布尔可达,以及顶格时的参照物
这道题根本没有「价值」,把体积当价值只是为了套模板。
更贴题意的记法是布尔可达:f[j] =「体积和恰好为 j」凑不凑得出来。
而状态一旦只剩一个 bit,一件物品就能一句话推完:
2³⁰ 的暴力在顶格跑不动,那题面顶格上怎么对拍?
换一个「和 DP 一行代码都不共享、又跑得动」的算法就行 ——
折半枚举:30 件劈成两半,各枚举 2¹⁵ = 32768 个子集,一半排序、另一半逐个二分。
⇒ 和 P1803(换一个复杂度相同、选法完全不同的算法)、 P1223(同一个算法换一个类型)凑成第三种: 「对拍只能跑小数据」的主语从来不是对拍,是「你把暴力当成了唯一的参照物」。
★ 顺带一条:DP 是 O(nV),吃的是 V;折半枚举是 O(2^(n/2)·n),吃的是 n。
两把尺子量的不是同一个东西 —— 要是这道题的 V 开到 10¹⁸ 而 n 仍是 30,
该上的就是折半枚举了。
n = 30、体积 ∈ [1, 20000] 随机 300 轮,DP 和折半枚举不一致 0 组 —— 看着很稳。
可是数一下答案本身:
顶格档(n = 30,V = 20000,各 300 轮) |
答案就是 0 的轮数 |
|---|---|
| 体积 ∈ [1, 20000] | 129 / 300 |
| 体积 ∈ [1, 3000] | ★ 300 / 300 |
| 体积 ∈ [10000, 20000] | 0 / 300 |
东西一多就随便凑得满 ⇒ 四成的轮次两边都在输出 0。 第二档更极端:整整 300 轮全是平凡的 0,那一档对拍等于没跑。
⇒ 这是第 14 章 P1746 那条「一致有两种:都算对了,和都没算」的又一次。 要让顶格对拍问出问题,得把体积下限也抬起来。
5★★★ 错法二:一维写成正序 —— 同一个旋钮,两个 bug 走相反的方向
第 23 章第 ⑧ 步那句「倒序」写成正序,这一版就成了完全背包:
// P1049 错法二:一维写成正序 —— 它精确地解了另一道题//// 把第 23 章那句「倒序」写成正序,这一版就变成了**完全背包**:// 每一捆……不对,每一件物品可以被**重复装进箱子任意多次**。//// ⇒ 于是它算出来的「装进去的体积」只会更大(或相等),剩余空间只会更小(或相等)。// **它不是随机地错,它恒等于「每件物品有无限多个」那道题的答案。**// (解析页第 ⑤ 步把这句话量成了 300 组逐组相等。)//// ★ 而在这道题上,这个 bug 有一个白送的推论:// 只要**存在某件物品的体积能整除某段空隙**,正序就可能把箱子塞得更满 ——// 最极端的情形是有一件体积为 1 的物品,那它一定能把箱子塞满、输出 0。
#include <bits/stdc++.h>using namespace std;
int main() { int V, n; if (!(cin >> V >> n)) return 0; vector<int> v(n); for (int& x : v) cin >> x;
vector<int> f(V + 1, 0); for (int i = 0; i < n; i++) for (int j = v[i]; j <= V; j++) // ⚠ 正序 —— 同一件物品会被反复拿 f[j] = max(f[j], f[j - v[i]] + v[i]);
cout << V - f[V] << "\n"; return 0;}点「运行 ▶」看结果
★ 先把它「算了什么」说清楚 —— 它不是随机地错:
| 随机 300 组:正序的答案 vs 显式写的完全背包 | ★ 300 / 300 逐组相等 |
⇒ 每件物品可以被重复装进箱子任意多次。于是箱子只会更满、剩余只会更小。 官方样例上它也输出 0,照样被放过。
V = 40、n ≤ 12 固定,只拧体积上限,每档 300 轮:
| 体积上限 | 3 | 5 | 10 | 20 | 39 |
|---|---|---|---|---|---|
V / 体积上限 |
13.3 | 8.0 | 4.0 | 2.0 | 1.0 |
| 贪心「先拿大的」被抓 | ★ 0 | 0 | 27 | 93 | 124 |
| 「正序 = 完全背包」被抓 | ★ 300 | 289 | 173 | 114 | —— |
两行的方向正好相反。 为一个 bug 精心造的档位,正是另一个 bug 的盲区 (P1638 那天撞过一次)—— 而这一次,两个盲区在同一个旋钮的两头。
⚠⚠ 更要紧的是:那两个 0 不是同一种 0,而分开它们只要多跑几轮:
| 那一档加深到两万轮 | |
|---|---|
| 体积 ≤ 5 | 31 次(0.16%)—— 概率低而已,300 轮抓 0 次是意料之中 |
| 体积 ≤ 3 | ★ 0 次 —— 而这个 0 是一句乘法算出来的 |
体积 ≤ 3 那一档:n ≤ 12 件 × 每件 ≤ 3 = 总体积 ≤ 36 < V = 40 ——
箱子装得下全部,贪心和正解都是「全拿」,结构上不可能不同。
⇒ 又一次「『对拍 0 次』有两种原因」,而这一次两种在同一张表上并排出现。
6和算法无关、但会让你 WA 的那一条
// P1049 错法三:算对了,但**忘了最后那一下减法**//// f[V] 是「最多能装进去多少体积」,而题目问的是「箱子还剩多少空间」。// 这一版把 f[V] 直接打了出来。//// ★ 它是这一页四个错法里**唯一一个被官方样例当场挡住**的:// 样例答案是 0,而它打出 24。// ⇒ 又一次「[官方样例是个『一测就死』的过滤器](/sol/p1223/)」:// 它挡住的都是「每一组都错」的错法,放过的都是「偶尔才错」的。// 而真正让你 WA 在第 7 个点上的,恰恰是后一种。
#include <bits/stdc++.h>using namespace std;
int main() { int V, n; if (!(cin >> V >> n)) return 0; vector<int> v(n); for (int& x : v) cin >> x;
vector<int> f(V + 1, 0); for (int i = 0; i < n; i++) for (int j = V; j >= v[i]; j--) f[j] = max(f[j], f[j - v[i]] + v[i]);
cout << f[V] << "\n"; // ⚠ 打的是「装进去多少」,不是「剩下多少」 return 0;}点「运行 ▶」看结果
f[V] 是「装进去多少体积」,题目问的是「箱子剩下多少空间」。
少写一个 V -,样例当场打出 24 而不是 0。
本页三个错法,样例只挡住了这一个 —— 而它恰好是每一组都错的那种。 另外两个(贪心、正序)都是偶尔才错,样例一个都没拦住。
⇒ 这条规律至此连着七道题成立(P1223 起): 样例挡住的都是「每组都错」的,放过的都是「偶尔才错」的 —— 而真正让你 WA 在第 7 个点上的,恰恰是后一种。
7度量程序和生成器
8一页纸
| 关键的一步 | 只有体积没有价值 ⇒ 把体积同时当价值;答案是 V - f[V] |
| 哪一版能 AC | p1049.cpp —— 一维倒序 O(nV) = 6 × 10⁵(2³⁰ 是它的 1789 倍) |
| 等价写法 | 布尔可达(更贴题意)/ bitset 一句 f |= f << v(常数 1/64) |
| ★★ 顶格参照物 | 折半枚举 O(2^(n/2)·n) —— 暴力不是唯一的参照物(第三种办法) |
| 错法一 | 贪心先拿大的;样例放过,最小反例 V=10 / 6 5 5 |
| 错法二 | 一维正序 ⇒ 恒等于完全背包(300/300 逐组相等);样例也放过 |
| 错法三 | 忘了 V -;★ 唯一被样例挡住的那个(也是唯一「每组都错」的那个) |
| ★★★ 暴力的假象 | 加一句剪枝后,随机数据 2 万次、体积全 ≤ 3 时 2ⁿ⁺¹−1 次(顶格 21 亿)⇒ 「暴力能不能过」的主语是数据形状,不是 n |
| ★★★ 生成器 | 同一个旋钮(体积上限)把两个 bug 推向相反方向:3 → 贪心 0 / 正序 300;39 → 贪心 124 |
| ⚠ 两种 0 | 体积 ≤ 5 是概率低(两万轮 31 次);体积 ≤ 3 是结构性(12 × 3 = 36 < 40) |
| ⚠ 顶格的坑 | 体积 ∈ [1,20000] 时 129/300 轮答案就是 0 —— 四成在验一个平凡值 |