0-1 Knapsack Problem
0-1 Knapsack Problem(0-1 背包问题)
核心结论
对每件物品只有“选择”和“不选择”两种决策。动态规划按照物品逐个扩展状态,对两种决策产生的价值取最大值。
二维实现的时间复杂度为 、空间复杂度为 ;使用滚动数组后,空间可优化为 。
1. 问题定义
给定 件物品,物品索引为 :
- 物品 的重量为 ;
- 物品 的价值为 ;
- 背包容量为 ;
- 每件物品最多选择一次,并且不能拆分。
使用决策变量 表示是否选择物品 ,则问题可以形式化为:
满足:
其中:
- :选择物品 ;
- :不选择物品 。
“0-1”的含义
“0-1”表示每件物品只能选择 次或 次,而不是指物品的重量或价值只能为 0 或 1。
2. 基本思想
0-1 背包是典型的Dynamic Programming 动态规划问题。
处理物品 时,对每个容量 ,只有两种可能:
- 不选择物品 :最优价值保持为只考虑前面物品时的结果;
- 选择物品 :需要从容量中扣除 ,并增加价值 。
因此,当前状态的最优值等于两种决策中的较大值:
不能直接使用单位重量价值贪心
0-1 背包通常不具有贪心选择性质。按照 从大到小选择可能错过全局最优解。
可以按单位重量价值贪心求解的是允许拆分物品的分数背包问题,不是 0-1 背包。
3. 动态规划状态设计
3.1 二维状态定义
定义:
表示:
只允许使用物品 ,且背包容量上限为 时,可以获得的最大总价值。
其中:
- ;
- ;
dp的大小为 。
这里第二维需要 个位置,因为容量状态包括 和 。
3.2 边界条件
只考虑物品 时:
即:
- 当前容量装不下物品 ,价值为 0;
- 当前容量能够装下物品 ,选择它可获得价值 。
3.3 状态转移方程
对于物品 ,其中 :
情况一:当前容量装不下物品
当 时,只能不选择物品 :
情况二:当前容量能够装下物品
当 时:
- 不选择物品 :价值为 ;
- 选择物品 :价值为 。
因此:
完整转移方程为:
最终答案为:
4. 具体过程
- 创建大小为 的二维数组
dp,初始值均为 0。 - 根据物品 的重量和价值初始化
dp[0][c]。 - 按照物品索引 依次处理物品。
- 对每个容量 :
- 若 ,不能选择当前物品;
- 否则比较选择与不选择当前物品的价值。
- 将较大的价值写入
dp[i][c]。 - 返回
dp[n-1][W]。
graph TD
A[开始处理物品 i 和容量 c] --> B{w_i <= c?}
B -->|否| C[dp i c = dp i-1 c]
B -->|是| D[计算不选择价值]
D --> E[计算选择价值]
E --> F[取两者最大值]
C --> G[处理下一个状态]
F --> G
5. 伪代码
5.1 二维动态规划
ZERO-ONE-KNAPSACK(weights, values, W)
n <- length(weights)
if n = 0 or W = 0
return 0
dp <- n x (W + 1) array filled with 0
// 初始化:只允许使用物品 0
for c <- weights[0] to W
dp[0][c] <- values[0]
// 依次处理物品 1 到 n - 1
for i <- 1 to n - 1
for c <- 0 to W
dp[i][c] <- dp[i - 1][c]
if weights[i] <= c
take <- dp[i - 1][c - weights[i]] + values[i]
dp[i][c] <- max(dp[i][c], take)
return dp[n - 1][W]
5.2 一维空间优化
观察状态转移可知,第 行只依赖第 行,因此可以将二维数组压缩为一维数组。
定义:
表示处理完当前范围内的物品后,在容量上限为 时能够获得的最大价值。
ZERO-ONE-KNAPSACK-OPTIMIZED(weights, values, W)
n <- length(weights)
dp <- array[0 ... W] filled with 0
for i <- 0 to n - 1
for c <- W downto weights[i]
dp[c] <- max(
dp[c],
dp[c - weights[i]] + values[i]
)
return dp[W]
容量必须倒序遍历
一维 0-1 背包中,容量必须从 递减到 。
若容量从小到大遍历,较小容量处刚更新的
dp会被当前物品再次使用,相当于允许同一物品被选择多次,算法就会退化为完全背包问题的转移方式。
6. 为什么一维状态能够正确工作
二维转移中,选择物品 时使用的是上一行状态:
将二维数组压缩为一维后,必须保证读取 dp[c-w_i] 时,它仍然表示“尚未处理当前物品 ”的状态。
由于 ,当容量从大到小更新时:
- 当前正在更新较大的
dp[c]; - 较小的
dp[c-w_i]尚未被当前物品更新; - 因此物品 不会被重复选择。
这正是倒序遍历的本质。
7. 正确性说明
7.1 最优子结构
考虑状态 的一个最优解。
- 若最优解不包含物品 ,则该解完全来自物品 ,其价值为 ;
- 若最优解包含物品 ,删除物品 后,剩余部分必须是在容量 下使用物品 的最优解,其价值为 。
否则,可以用一个更优的子问题解替换剩余部分,从而得到更优的原问题解,与原解最优矛盾。
因此状态转移覆盖了最优解的全部可能情况。
7.2 数学归纳
对物品索引 进行归纳:
- 基础情况: 时,只有选择或不选择物品 两种情况,初始化正确;
- 归纳假设:假设第 行的所有状态均正确;
- 归纳步骤:第 行分别考虑不选择和选择物品 ,并取最大值,因此第 行也正确。
所以最终状态 是原问题的最优值。
8. 时空复杂度
设物品数量为 ,背包容量为 。
| 实现方式 | 时间复杂度 | 空间复杂度 | 是否便于恢复所选物品 |
|---|---|---|---|
| 暴力枚举所有子集 | 取决于实现 | 是 | |
| 二维动态规划 | 是 | ||
| 一维动态规划 | 通常不便 |
重要
是伪多项式复杂度 输入容量 若采用二进制表示,只需要 位。
因此 是关于数值 的多项式,而不一定是关于输入位数的多项式。这类复杂度称为伪多项式时间复杂度(pseudo-polynomial time)。
9. 问题需要具有的性质
9.1 动态规划所需性质
最优子结构
原问题的最优解可以由规模更小的背包子问题的最优解构造。
重叠子问题
递归搜索过程中,相同的“物品范围 + 剩余容量”状态会被多次计算,适合使用 dp 表保存结果。
状态数量可控
标准容量型动态规划共有约 个状态。当 不过大时,这些状态可以被枚举和存储。
9.2 标准转移成立所需的建模条件
- 每件物品最多选择一次;
- 物品不能拆分;
- 总重量是各物品重量之和;
- 总价值是各物品价值之和;
- 物品之间不存在额外的依赖、互斥或组合奖励;
- 重量和容量通常应为非负整数,以便将容量作为数组下标;
- 一般假设 ,否则需要单独处理零重量物品。
标准模型的失效情况
若物品之间存在“选择 A 后才能选择 B”、互斥关系、分组限制或非线性组合收益,标准 0-1 背包状态通常不足,需要增加状态维度或改用其他模型。
9.3 不需要具备贪心选择性质
动态规划只要求最优子结构和可复用的重叠子问题,不要求每一步的局部最优选择必然属于某个全局最优解。
10. 典型例子
给定 4 件物品:
| 物品索引 | 重量 | 价值 |
|---|---|---|
| 0 | 2 | 3 |
| 1 | 3 | 4 |
| 2 | 4 | 5 |
| 3 | 5 | 6 |
背包容量:
10.1 二维 dp 表
表中第 行表示只允许使用物品 时的结果。
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 0 | 3 | 3 | 3 | 3 | 3 | 3 | 3 |
| 1 | 0 | 0 | 3 | 4 | 4 | 7 | 7 | 7 | 7 |
| 2 | 0 | 0 | 3 | 4 | 5 | 7 | 8 | 9 | 9 |
| 3 | 0 | 0 | 3 | 4 | 5 | 7 | 8 | 9 | 10 |
10.2 关键状态计算
计算 时,当前物品为物品 3:
- 不选择物品 3:
- 选择物品 3:
因此:
最优方案为选择:
- 物品 1:重量 3,价值 4;
- 物品 3:重量 5,价值 6。
总重量:
总价值:
11. 从二维 dp 表恢复一个最优方案
从状态 逆向检查:
- 若 ,可以不选择物品 ;
- 若 ,说明某个最优方案选择了物品 ,随后令 。
RECONSTRUCT(weights, values, W, dp)
n <- length(weights)
selected <- empty list
c <- W
for i <- n - 1 downto 1
if dp[i][c] != dp[i - 1][c]
append i to selected
c <- c - weights[i]
if c >= weights[0] and dp[0][c] = values[0]
append 0 to selected
reverse(selected)
return selected
多个最优解
当“选择”和“不选择”得到相同价值时,问题可能存在多个最优方案。上述回溯规则只恢复其中一个。
12. 常见错误
12.1 将一维容量正序遍历
错误写法:
for c <- weights[i] to W
这会让同一物品在一轮中被重复使用。
正确写法:
for c <- W downto weights[i]
12.2 混淆容量与实际装入重量
dp[i][c] 中的 表示允许使用的容量上限,不要求最优方案恰好装满到 。
12.3 误认为 一定优于
当 极大而 较小时,容量型动态规划可能比枚举、Backtracking 回溯法或Branch and Bound 分支限界法更慢。算法选择应同时考虑 与 的规模。
12.4 将 0-1 背包与其他背包模型混淆
| 模型 | 每件物品可选次数 | 一维容量遍历方向 |
|---|---|---|
| 0-1 背包 | 最多 1 次 | 从大到小 |
| 完全背包 | 任意多次 | 从小到大 |
| 多重背包 | 有限多次 | 需拆分或使用专门优化 |
| 分数背包 | 可选择一部分 | 通常使用贪心 |
13. 方法对比
| 方法 | 核心搜索空间 | 典型复杂度 | 特点 |
|---|---|---|---|
| 暴力枚举 | 所有物品子集 | 简单但指数级 | |
| 动态规划 | 物品索引与容量 | 稳定,适合容量不大的整数输入 | |
| Backtracking 回溯法 | 二叉子集树 | 最坏 | 可通过约束和上界剪枝 |
| Branch and Bound 分支限界法 | 活结点与优先队列 | 最坏仍为指数级 | 优先扩展更可能产生最优解的结点 |
14. 复习检查
- 能写出 0-1 背包的数学模型。
- 能准确解释
dp[i][c]的含义。 - 能推导选择与不选择两种转移。
- 能说明一维优化为什么必须倒序遍历容量。
- 能区分 0-1 背包、完全背包和分数背包。
- 能解释为什么 是伪多项式复杂度。
- 能从二维
dp表恢复一个最优物品集合。
15. 相关笔记
16. 参考资料
- Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2009). Introduction to Algorithms (3rd ed.). MIT Press,Chapter 15: Dynamic Programming.
- 课程资料:
greedy.ppt。其中对比了 0-1 背包与分数背包,指出前者通常不具备贪心选择性质。 - 课程资料:
第5章 回溯法.pdf,第 5.6 节“0-1 背包问题”,用于对比动态规划与回溯求解框架。 - 课程资料:
第6章 分支限界法.pdf,用于对比分支限界法求解 0-1 背包的思路。 - Obsidian Flavored Markdown Skill,用于本文档的 Obsidian Markdown 格式规范。