基础背包问题
一、问题定义
背包问题是组合优化中的经典问题:
给定一个容量有限的背包和若干物品(每个物品有重量和价值),如何选择物品装入背包,使得总价值最大且不超过背包容量?
二、背包问题的分类
背包问题
├── 0/1 背包 → 每个物品最多选 1 次
├── 完全背包 → 每个物品可选无限次
├── 多重背包 → 每个物品有数量限制
├── 分组背包 → 物品分组,每组最多选一个
└── 混合背包 → 以上类型的组合
三、核心逻辑
由问题定义出发,该问题主要是由两个维度(容量[物品重量和]、价值)组成的优化问题,解决它的关键也是由这两个维度构建的状态转移方程,通过它求解(容量、最大价值)。
以基础的0/1背包为例,最直接的思路就是枚举(容量、价值)这个状态矩阵,根据状态转移方程更新这个矩阵。
其转移方程为:
对于第 i 个物品,只有两种选择:
① 不选第 i 个物品: dp[i][j] = dp[i-1][j]
② 选第 i 个物品: dp[i][j] = dp[i-1][j-w[i]] + v[i] (前提: j >= w[i])
dp[i][j] = max(不选, 选)
对于二维DP的话,这个方程就非常简单,顺序更新即可。直观理解就是遍历每个容量值,尝试放入物品并取最大价值。可视化该状态矩阵更新:
容量j → 0 1 2 3 4 5 6 7 8 9 10
物品0(w=2,v=6) 0 0 6 6 6 6 6 6 6 6 6
物品1(w=5,v=12) 0 0 6 6 6 12 12 18 18 18 18
物品2(w=4,v=8) 0 0 6 6 8 12 14 18 18 20 20
物品3(w=7,v=15) 0 0 6 6 8 12 14 18 18 21 21
↑
答案=21?
实际 dp[4][10] = max(21, ...) 需要重新验证 → 答案是 21
可以看到二维状态矩阵是有一部分信息冗余的,冗余的部分就是保存了枚举到的每个物品对应的中间价值,我们只关心最终所有物品都遍历后的结果。这就产生了一维DP的空间优化:
使用一维数组保存 每个容量 对应的最大价值 (\(f(volume) \rightarrow Worth_{max}\)) ,枚举容量的时候需要逆序,因为转移方程是依赖前序容量的value的,二维DP将该值按物品隔离了,一维是压缩在一起,因此只能逆序更新。
def knapsack_01_optimized(W, weights, values):
n = len(weights)
dp = [0] * (W + 1)
for i in range(n):
# ⭐ 必须逆序遍历,防止同一物品被重复选取
for j in range(W, weights[i] - 1, -1):
dp[j] = max(dp[j], dp[j - weights[i]] + values[i])
return dp[W]
四、完全背包
完全背包的关键就是每个物品可以重复选,上面也提示到了核心区别,在0/1背包中,枚举当前物品时,只能从上一个物品的状态进行更新,而一维优化时,需要逆序来避免重复选择。因此完全背包的唯一区别就是可以从 当前物品 的状态进行更新(当前物品可重复用),并且一维DP时直接正序更新就可以。
def knapsack_01_optimized(W, weights, values):
n = len(weights)
dp = [0] * (W + 1)
for i in range(n):
# ⭐ 必须逆序遍历,防止同一物品被重复选取
for j in range(weights[i], W + 1):
dp[j] = max(dp[j], dp[j - weights[i]] + values[i])
return dp[W]
拓展
对于其他如多重背包,每种物品有上限,就是多一个限制条件,对于每种物品的容量来说,就是多枚举一轮物品个数即可。
变种
还有其他问题不只是求解最大value,也有求最大方案数的。

浙公网安备 33010602011771号