【动态规划】背包问题
背包问题
简介
简单来讲,你有一个背包,它的容量为
背包问题的分类:
问题分类 | 区别 |
---|---|
0-1背包 | 每种物品的数量是一件 |
完全背包 | 每种物品的数量是无限件 |
多重背包 | 每种物品的数量是有限件 |
0-1背包问题
题目
P2871 [USACO07DEC]Charm Bracelet S
有
件物品和一个容量为 的背包。第 件物品的重量是 ,价值是 。求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。
用例1:
输入:
W = 10, w =[5, 6, 2, 3, 4], v = [6, 12, 5, 6, 6]
输出:
18
分析
假设
边界条件
显然,当物品数量为零时,不管背包的容量是多少,最大价值都是零,同理,当背包容量为零时,不管有多少个物品,最大价值也都是零。
因此,边界条件:
我们以题目中的用例为例,将初始状态,填入表格中,即:
状态转移方程
由于每种物品只有一件,我们可以选择放或者不放。因此,对于第
- 放入背包,对应的价值就是:
; - 不放入背包,对应的价值就是:
。
注意:第
那么,最大价值,就是这两种状态中选择一个价值最大的方案即可,即:
我们以
因此,就有
代码实现
from typing import List def knapsack(w: int, weight: List[int], value: List[int]): n = len(weight) dp = [[0] * (w + 1) for _ in range(n + 1)] for i in range(1, n + 1): for j in range(1, w + 1): if j < weight[i - 1]: dp[i][j] = dp[i - 1][j] else: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i - 1]] + value[i - 1]) return dp[n][w] if __name__ == "__main__": print(knapsack(10, [5, 6, 2, 3, 4], [6, 12, 5, 6, 6]))
由于背包容积小于当前物品的体积时,能装下的物品价值为零,所以,上述代码也等价于:
def knapsack(w: int, weight: List[int], value: List[int]): n = len(weight) dp = [[0] * (w + 1) for _ in range(n + 1)] for i in range(1, n + 1): for j in range(1, w + 1): if j >= weight[i - 1]: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i - 1]] + value[i - 1]) return dp[n][w]
复杂度
- 时间复杂度:
- 空间复杂度:
0-1背包的优化
分析
通过观察,我们可以发现,对于
因为,当前状态
我们以
因此,就有
压缩状态之后,需要将
可以看出
对于当前处理的物品个数为
如果我们对背包的容量
代码实现
from typing import List def knapsack(w: int, weight: List[int], value: List[int]): n = len(weight) dp = [0] * (w + 1) for i in range(1, n + 1): for j in range(w, -1, -1): if j >= weight[i - 1]: dp[j] = max(dp[j], dp[j - weight[i - 1]] + value[i - 1]) return dp[w] if __name__ == "__main__": print(knapsack(10, [5, 6, 2, 3, 4], [6, 12, 5, 6, 6]))
复杂度
- 时间复杂度:
- 空间复杂度:
完全背包问题
题目
有
种物品和一个容量为 的背包。第 种物品的重量是 ,价值是 ,每种物品的数量都有无限个。求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。
分析
假设
边界条件
显然,当物品数量为零时,不管有多少个物品,最大价值都是零,同理,当背包容积为零时,不管背包的体积是多大,最大价值都是零。
因此,边界条件:
状态转移方程
由于每种物品,我们可以选择放入多次或者不放。因此,对于第
- 不放入的情况
与前面的 0-1背包 的场景类似,即: - 放入的情况
我们可以考虑最朴素的做法:对于第 种物品, 枚举其选择了多少个来转移,即:注意,这里需要保证 ,这种方式的的时间复杂度是 。
我们换一个思路考虑,由于 已经由 更新过了。那么 就是充分考虑了 第 种物品所选次数之后得到的最优结果,所以,状态转移方程可以优化为:换言之,我们通过局部最优子结构的性质重复使用了之前的枚举过程,优化了枚举的复杂度。
综上,对于上述两种情况,状态转移方程就可以优化为:
也就是说,由于每种物品有无限个,所以,状态不应该从
这里,我们通过公式,简单地说明推导过程:
而根据递推公式,可以计算
可以看出,上述两个等式中,花括号标记的部分,相差
我们以
因此,就有
代码实现
from typing import List def knapsack(w: int, weight: List[int], value: List[int]): n = len(weight) dp = [[0] * (w + 1) for _ in range(n + 1)] for i in range(1, n + 1): for j in range(1, w + 1): if j < weight[i - 1]: dp[i][j] = dp[i - 1][j] else: dp[i][j] = max(dp[i - 1][j], dp[i][j - weight[i - 1]] + value[i - 1]) return dp[n][w] if __name__ == "__main__": print(knapsack(10, [5, 6, 2, 3, 4], [6, 12, 5, 6, 6]))
复杂度
- 时间复杂度:
- 空间复杂度:
完全背包的优化
分析
与 0-1背包相同,我们可以将第一维去掉来优化空间复杂度。
注意,去掉第一维之后,对背包容量
如果理解了 0-1背包 的优化方式,就不难明白压缩后的循环是正向的(也就是上文中提到的错误优化)。
代码实现
from typing import List def knapsack(w: int, weight: List[int], value: List[int]): n = len(weight) dp = [0] * (w + 1) for i in range(1, n + 1): for j in range(1, w + 1): if j >= weight[i - 1]: dp[j] = max(dp[j], dp[j - weight[i - 1]] + value[i - 1]) return dp[w]
多重背包问题
题目
有
种物品和一个容量为 的背包。第 种物品的重量是 ,价值是 ,每种物品的数量有 个。求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。
分析
假设
边界条件
显然,当物品数量为零时,不管有多少个物品,最大价值都是零,同理,当背包容积为零时,不管背包的体积是多大,最大价值都是零。
因此,边界条件:
状态转移方程
由于第
压缩状态
通过压缩状态的方式,对空间复杂度进行优化:
代码实现
from typing import List def knapsack(w: int, weight: List[int], value: List[int], nums: List[int]): n = len(weight) dp = [[0] * (w + 1) for _ in range(n + 1)] for i in range(1, n + 1): for j in range(w, -1, -1): for k in range(1, nums[i - 1] + 1): if j >= weight[i - 1] * k: dp[i][j] = max( dp[i - 1][j], dp[i - 1][j - weight[i - 1] * k] + value[i - 1] * k ) return dp[n][w] if __name__ == "__main__": print(knapsack(10, [5, 6, 2, 3, 4], [6, 12, 5, 6, 6], [1, 2, 2, 2, 2]))
复杂度
- 时间复杂度:
- 空间复杂度:
通过压缩状态,优化空间复杂度的实现:
from typing import List def knapsack(w: int, weight: List[int], value: List[int], nums: List[int]): n = len(weight) dp = [0] * (w + 1) for i in range(1, n + 1): for j in range(w, -1, -1): for k in range(1, nums[i - 1] + 1): if j >= weight[i - 1] * k: dp[j] = max(dp[j], dp[j - weight[i - 1] * k] + value[i - 1] * k) return dp[w] if __name__ == "__main__": print(knapsack(10, [5, 6, 2, 3, 4], [6, 12, 5, 6, 6], [1, 2, 2, 2, 2]))
复杂度
- 时间复杂度:
- 空间复杂度:
多重背包的优化
分析
在多重背包问题中,我们假设
例如,对于第
我们知道,对于任意的十进制数,都可以转换成二进制形式,即:
多重背包问题,可以通过 二进制分组 的方式进行优化。
例如,假设物品的数量是
分组 | 0 | 1 | 2 | 3 |
---|---|---|---|---|
数量 | 1 | 2 | 4 | 8 |
我们将每一组都看成是一个新的物品,那么,多重背包 就可以转换成 0-1 背包 的模型了。
分组的时候,有些数字,并不能刚好分成若干个
例如,
代码实现
from typing import List, Tuple def binary_divide(w: int, v: int, count: int) -> List[Tuple]: """ 二进制分组 :param w: 物品的体积 :param v: 物品的价值 :param count: 物品的数量 :return: 分组后的结果, (物品的体积之和,物品的价值之和,该组物品的数量) """ divide = list() current = 0 while count: current = 1 << current if count >= current: count -= current divide.append((w * current, v * current, current)) else: divide.append((w * count, v * count, count)) break return divide def knapsack(w: int, weight: List[int], value: List[int], nums: List[int]): items = list() for i in range(len(weight)): items.extend(binary_divide(weight[i], value[i], nums[i])) n = len(items) dp = [[0] * (w + 1) for _ in range(n + 1)] for i in range(1, n + 1): for j in range(w, -1, -1): if j >= items[i - 1][0]: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - items[i - 1][0]] + items[i - 1][1]) return dp[n][w] if __name__ == "__main__": print(knapsack(10, [5, 6, 2, 3, 4], [6, 12, 5, 6, 6], [1, 1, 1, 1, 1])) print(knapsack(10, [5, 6, 2, 3, 4], [6, 12, 5, 6, 6], [2, 2, 2, 2, 2]))
复杂度
- 时间复杂度:
- 空间复杂度:
进一步对其压缩状态,优化后的的代码实现:
def knapsack(w: int, weight: List[int], value: List[int], nums: List[int]): items = list() for i in range(len(weight)): items.extend(binary_divide(weight[i], value[i], nums[i])) n = len(items) dp = [0] * (w + 1) for i in range(1, n + 1): for j in range(w, -1, -1): if j >= items[i - 1][0]: dp[j] = max(dp[j], dp[j - items[i - 1][0]] + items[i - 1][1]) return dp[w]
复杂度
- 时间复杂度:
- 空间复杂度:
总结
遍历顺序
0-1 背包的代码优化后,内层循环的顺序要从大到小的遍历,为了保证每个物品仅被添加一次。
完全背包的的代码优化后,内层循环的顺序要从小到大遍历,保证物品可以多次添加。
遍历嵌套方式
一般我们求背包所能容纳的最大价值的时候,都是在求物品的组合,而不关注物品的顺序,如果要关注物品的顺序,就需要求排列数。
总结:
如果求组合数就是外层 for 循环遍历物品,内层 for 遍历背包的容积。
如果求排列数就是外层 for 遍历背包的容积,内层 for 循环遍历物品。
参考:
本文作者:LARRY1024
本文链接:https://www.cnblogs.com/larry1024/p/17022316.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步