完全背包问题
如果每种物品有无限个,则是完全背包问题。
状态转移方程
每次决策时,可以从中物品中选择一个,下次决策时,还可重复选,这就等价于无限个了。因此可选的动作有种。
取出一个物品后,由于每种物品的个数是无限个,所以物品的个数没有发生变化,依旧是无限的,但是背包的容量变小了,这里唯一的变量就是背包容量。因此我们定义函数 ,表示背包容量为 时能容纳的物品的最大价值。
至此,我们回答了4个问题:
- 原子问题是 ,即背包容量为0时,能容纳的物品的最大价值为0
- 变量为背包容量
- 函数的返回值为能容纳的物品的最大价值
- 可选的动作有种:对每种物品,选或不选
因此,可以得出状态转移方程如下:
在上面的公式中,第一个表示不选物品,第二个 表示选择物品后的最优解。如果更大,max()
运算符会选择,不选第种物品;如果更大,则会选择第种物品。由于 每次可以重复选,精准表达了每种物品有无限个可用的意思。
代码框架
根据状态转移方程,得出伪代码如下:
dp = [0] * (W+1)
for i in range(N):
for w in range(w[i], W+1):
dp[w]=max(dp[w],dp[j-w[i]]+v[i])
与 0-1 背包问题相比,仅有一行代码不同,这里内循环是顺序的,而 0-1 背包是逆序的(在使用滚动数组的情况下)。
为什么?在0-1背包问题中,内循环从右到左,是为了保证每个物品只选一次,保证在选择第i
个物品时,依赖的是一个没有选择第i
个物品的子结果f[i-1][j-w[i]]
;而在完全背包问题中,内循环是从左到右的,选择第i
个物品时,依赖了左边的f[i-1][j-w[i]]
,它已经早先被更新了(因为从左到右更新的),此时f[i-1][j-w[i]]
这个最优解,有可能选择了i
物品,意味着重复选择了物品i
,因此精准表达了物品有无限个。
抽象出处理单个物品的函数:
def unbounded_knapsack(f: List[int], i: int):
for j in range(w[i], W+1):
f[j]=max(f[j],f[j-w[i]]+v[i])
内外for循环的顺序
- 求最值,内外for循环可以任意颠倒。例如 Coin Change
- 求组合数,外层for遍历物品,内层for循环遍历背包。例如 Coin Change II
- 求排列数,外层for遍历背包,内层for循环遍历物品。例如 Combination Sum IV