完全背包问题:有
$n$ 种物品和一个最多能装重量为$W$ 的背包,第$i$ 种物品的重量为$weight[i]$ ,价值为$value[i]$ ,每种物品数量没有限制。请问在总重量不超过背包载重上限的情况下,能装入背包的最大价值是多少?
完全背包问题的特点:每种物品有无限件。
我们可以参考「0-1 背包问题」的状态定义和基本思路,对于容量为
按照物品种类的序号、当前背包的载重上限进行阶段划分。
定义状态
状态
由于每种物品可选的数量没有限制,因此状态
- 选择
$0$ 件第$i - 1$ 件物品:可以获得的最大价值为$dp[i - 1][w]$ - 选择
$1$ 件第$i - 1$ 件物品:可以获得的最大价值为$dp[i - 1][w - weight[i - 1]] + value[i - 1]$ 。 - 选择
$2$ 件第$i - 1$ 件物品:可以获得的最大价值为$dp[i - 1][w - 2 \times weight[i - 1]] + 2 \times value[i - 1]$ 。 - ……
- 选择
$k$ 件第$i - 1$ 件物品:可以获得的最大价值为$dp[i - 1][w - k \times weight[i - 1]] + k \times value[i - 1]$ 。
注意:选择
$k$ 件第$i - 1$ 件物品的条件是$0 \le k \times weight[i - 1] \le w$ 。
则状态转移方程为:
- 如果背包载重上限为
$0$ ,则无论选取什么物品,可以获得的最大价值一定是$0$ ,即$dp[i][0] = 0, 0 \le i \le size$ 。 - 无论背包载重上限是多少,前
$0$ 种物品所能获得的最大价值一定为$0$ ,即$dp[0][w] = 0, 0 \le w \le W$ 。
根据我们之前定义的状态,$dp[i][w]$ 表示为:前
class Solution:
# 思路 1:动态规划 + 二维基本思路
def completePackMethod1(self, weight: [int], value: [int], W: int):
size = len(weight)
dp = [[0 for _ in range(W + 1)] for _ in range(size + 1)]
# 枚举前 i 种物品
for i in range(1, size + 1):
# 枚举背包装载重量
for w in range(W + 1):
# 枚举第 i - 1 种物品能取个数
for k in range(w // weight[i - 1] + 1):
# dp[i][w] 取所有 dp[i - 1][w - k * weight[i - 1] + k * value[i - 1] 中最大值
dp[i][w] = max(dp[i][w], dp[i - 1][w - k * weight[i - 1]] + k * value[i - 1])
return dp[size][W]
-
时间复杂度:$O(n \times W \times \sum\frac{W}{weight[i]})$,其中
$n$ 为物品种类数量,$W$ 为背包的载重上限,$weight[i]$ 是第$i$ 种物品的重量。 - 空间复杂度:$O(n \times W)$。
上之前的思路中,对于每种物品而言,每次我们都需要枚举所有可行的物品数目
实际上,我们可以对之前的状态转移方程进行一些优化,从而减少一下算法的时间复杂度。
我们将之前的状态转移方程
进行展开:
$(1) \quad dp[i][w] = max \begin{cases} dp[i - 1][w] \cr dp[i - 1][w - weight[i - 1]] + value[i - 1] \cr dp[i - 1][w - 2 \times weight[i - 1]] + 2 \times value[i - 1] \cr …… \cr \cr dp[i - 1][w - k \times weight[i - 1]] + k \times value[i - 1] \end{cases}, \quad 0 \le k \times weight[i - 1] \le w$
而对于
$(2) \quad dp[i][w - weight[i - 1]] = max \begin{cases} dp[i - 1][w - weight[i - 1]] \cr dp[i - 1][w - 2 \times weight[i - 1]] + value[i - 1] \cr dp[i - 1][w - 3 \times weight[i - 1]] + 2 \times value[i - 1] \cr …… \cr dp[i - 1][w - k \times weight[i - 1]] + (k - 1) \times value[i - 1] \end{cases}, \quad weight[i - 1] \le k \times weight[i - 1] \le w$
通过观察可以发现:
-
$(1)$ 式中共有$k + 1$ 项,$(2)$ 式中共有$k$ 项; -
$(2)$ 式整个式子与$(1)$ 式第$1 \sim k + 1$ 项刚好相差一个$value[i - 1]$ 。
则我们将
简化后的「状态转移方程」去除了对物品件数的依赖,也就不需要遍历
注意:式
$(3)$ 的满足条件为$0 \le weight[i - 1] \le w$ 。当$w < weight[i - 1]$ 时,$dp[i][w] = dp[i - 1][w]$。
则状态转移方程为:
$\quad dp[i][w] = \begin{cases} dp[i - 1][w] & w < weight[i - 1] \cr max \lbrace dp[i - 1][w], \quad dp[i][w - weight[i - 1]] + value[i - 1] \rbrace & w \ge weight[i - 1] \end{cases}$
从上述状态转移方程我们可以看出:该式子与 0-1 背包问题中「思路 1」的状态转移式极其相似。
唯一区别点在于:
- 0-1 背包问题中状态为
$dp[i - 1][w - weight[i - 1]] + value[i - 1]$ ,这是第$i - 1$ 阶段上的状态值。- 完全背包问题中状态为
$dp[i][w - weight[i - 1]] + value[i - 1]$ ,这是第$i$ 阶段上的状态值。
按照物品种类的序号、当前背包的载重上限进行阶段划分。
定义状态
状态
$\quad dp[i][w] = \begin{cases} dp[i - 1][w] & w < weight[i - 1] \cr max \lbrace dp[i - 1][w], \quad dp[i][w - weight[i - 1]] + value[i - 1] \rbrace & w \ge weight[i - 1] \end{cases}$
- 如果背包载重上限为
$0$ ,则无论选取什么物品,可以获得的最大价值一定是$0$ ,即$dp[i][0] = 0, 0 \le i \le size$ 。 - 无论背包载重上限是多少,前
$0$ 种物品所能获得的最大价值一定为$0$ ,即$dp[0][w] = 0, 0 \le w \le W$ 。
根据我们之前定义的状态,$dp[i][w]$ 表示为:前
class Solution:
# 思路 2:动态规划 + 状态转移方程优化
def completePackMethod2(self, weight: [int], value: [int], W: int):
size = len(weight)
dp = [[0 for _ in range(W + 1)] for _ in range(size + 1)]
# 枚举前 i 种物品
for i in range(1, size + 1):
# 枚举背包装载重量
for w in range(W + 1):
# 第 i - 1 件物品装不下
if w < weight[i - 1]:
# dp[i][w] 取「前 i - 1 种物品装入载重为 w 的背包中的最大价值」
dp[i][w] = dp[i - 1][w]
else:
# dp[i][w] 取「前 i - 1 种物品装入载重为 w 的背包中的最大价值」与「前 i 种物品装入载重为 w - weight[i - 1] 的背包中,再装入 1 件第 i - 1 种物品所得的最大价值」两者中的最大值
dp[i][w] = max(dp[i - 1][w], dp[i][w - weight[i - 1]] + value[i - 1])
return dp[size][W]
-
时间复杂度:$O(n \times W)$,其中
$n$ 为物品种类数量,$W$ 为背包的载重上限。 - 空间复杂度:$O(n \times W)$。
通过观察「思路 2」中的状态转移方程
$dp[i][w] = \begin{cases} dp[i - 1][w] & w < weight[i - 1] \cr max \lbrace dp[i - 1][w], \quad dp[i][w - weight[i - 1]] + value[i - 1] \rbrace & w \ge weight[i - 1] \end{cases}$
可以看出:我们只用到了当前行(第
所以我们没必要保存所有阶段的状态,只需要使用一个一维数组
按照当前背包的载重上限进行阶段划分。
定义状态
$dp[w] = \begin{cases} dp[w] & w < weight[i - 1] \cr max \lbrace dp[w], \quad dp[w - weight[i - 1]] + value[i - 1] \rbrace & w \ge weight[i - 1] \end{cases}$
注意:这里的
$dp[w - weight[i - 1]]$ 是第$i$ 轮计算之后的「第$i$ 阶段的状态值」。
因为在计算
因为
- 无论背包载重上限为多少,只要不选择物品,可以获得的最大价值一定是
$0$ ,即$dp[w] = 0, 0 \le w \le W$ 。
根据我们之前定义的状态,
class Solution:
# 思路 3:动态规划 + 滚动数组优化
def completePackMethod3(self, weight: [int], value: [int], W: int):
size = len(weight)
dp = [0 for _ in range(W + 1)]
# 枚举前 i 种物品
for i in range(1, size + 1):
# 正序枚举背包装载重量
for w in range(weight[i - 1], W + 1):
# dp[w] 取「前 i - 1 种物品装入载重为 w 的背包中的最大价值」与「前 i 种物品装入载重为 w - weight[i - 1] 的背包中,再装入 1 件第 i - 1 种物品所得的最大价值」两者中的最大值
dp[w] = max(dp[w], dp[w - weight[i - 1]] + value[i - 1])
return dp[W]
通过观察「0-1 背包问题滚动数组优化的代码」和「完全背包问题滚动数组优化的代码」可以看出,两者的唯一区别在于:
- 0-1 背包问题滚动数组优化的代码采用了「从
$W \sim weight[i - 1]$ 逆序递推的方式」。- 完全背包问题滚动数组优化的代码采用了「从
$weight[i - 1] \sim W$ 正序递推的方式」。
-
时间复杂度:$O(n \times W)$,其中
$n$ 为物品种类数量,$W$ 为背包的载重上限。 - 空间复杂度:$O(W)$。