背包问题
有N件物品和一个容量为V的背包。第i件物品的费用是c[i]
,价值是w[i]
。求解将哪些物品装入背包可使价值总和最大。
问题分类
- 01背包问题 如果
N
个物品每个物品只能使用一次,则成为01背包问题 - 完全背包问题 如果N
N
个物品每个物品可以使用无限次,则为完全背包问题
问题的解法
- 暴力搜索解法(递归解法)
- 动态规划解法
- 优化后的动态规划(滚动数组,自下而上)
01背包问题
有N件物品和一个容量为V的背包。第i件物品的费用是
c[i]
,价值是w[i]
。求解将哪些物品装入背包可使价值总和最大。 每件物品只能使用1次
问题分析
- 设 C(i,w) 为 当物品有
i
个的时候, 且背包重量在 w 的情况下 取得的最大值。那么考虑第i
个物品,无外乎两种可能:选,或者不选。- 不选的话,背包的容量不变,改变为问题
C(i -1, w)
; - 选的话,背包的容量变小,改变为问题
C(i -1, w- w[i])
。
最优方案就是比较这两种方案,哪个会更好些:
我们来看状态方程
- 不选的话,背包的容量不变,改变为问题
-
如何理解这个方程:
假设 我们有
c[3,4]
和 w[5,10]
,i[0,1] i
我们放每个元素的名称。首先对于
0
这个元素 重量为5
价值为3
。
假设
我们要求C(1,5)
代表 在背包重量为5
的情况下 从0,1
中选择价值最大的。 直观的结果当让是3
了。 -
分析过程
按照上述公式 当我要求C(1,5)
的时候,对于1
这个元素有两种求法1
这个元素被选中1
这个元素没有被选中
如果
1
被选中了,则背包的重量要减去1
的重量10
那么在接下来的问题就变成 在C(0,5 - 10) + 4
(4 是用于1已经被选中了,所以原问题就变成了在子问题基础上 加上 已经选中的价值) 但是C(0, -5)
不可能存在,所以1
不能选 所以 有了C(0,5)
的子问题 -
我们在看一个更复杂的例子:
ps = [12,3,1,3,6]
ws = [5,4,7,2,6]
i = [0,1,2,3,4]
(物品的代号)W = 10
我们依旧定义问题
C(4, 10)
根据公式,我们来公式
下面是具体的分析过程
4
被选中
4
没有被选中
求
C(4, 10)
- 如果
4
号元素被选中了,则进入子问题构成的问题
C(4 -1, 10 - ws[4]) + ps[4]
- 如果
4
号元素 没有被选中,则进入第个子问题C(4 -1, 10
- 这两个我们要取得最大的结果 才符合要求
- 处理边界 当
i
和v
有一个小于0
的时候 这个问题都是不存在的
- 如果
代码实现
递归写法
1 | # 0 -1 背包问题 |
运行结果
1 | ps = [12,3,1,3,6] |
动态规划写法
-
中间结果的保存
由于我们的问题是
C(i,w)
i
和w
会有各种不同的取值 那么我们申请一个二维数组来保存中间计算的结果
1 | # 由于我们的问题是`C(i,w)` `i` 和`w` 会有各种不同的取值 那么我们申请一个二维数组来保存中间计算的结果 |
运行结果
1 | ps = [12,3,1,3,6] |
- 优化分析
如果我们打印 ret_mid
v/i | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 15 | None | None | None | None | None | None | None | None | None | None |
1 | 9 | None | None | None | None | 3 | None | None | None | None | None |
2 | 9 | None | None | None | 6 | 3 | None | None | None | 0 | None |
3 | 9 | None | None | None | 6 | 3 | None | 3 | None | 0 | None |
4 | 6 | None | 6 | None | 6 | 0 | 0 | 0 | None | 0 | None |
这个表格的意思是,当我们选中 背包选中0
的时候 且这个时候背包的总量为 0
的时候 最大的价值 为15
既C(0,0)
的含义
同理C(1,0)
的意思是 没有选中 0
且 选中 1
和 不选 1
的价值最大的值
我们分析一下 如果我要求 C(i,w)
但是需要依赖于 C(i+1,?)
这里用?
表示只需要行数据。 那么需要上面数组 C(i+1,?)
的结果
也就是说 我们的 ret_mid[i] 要依赖于 ret_mid[i+1] 的结果 这个时候 我们的 ret_mid[i+1]
还没有被算出来
如果我们反转一下, 是否能计算将这个顺序 倒置一下,比如说让ret_mid[i]
依赖于 ret_mid[i - 1]
这样依赖,我们算到了ret_mid[0]
的时候,就可以被 ret_mid[1]
复用结果了。
我们来看一下代码
1 | # 由于我们的问题是`C(i,w)` `i` 和`w` 会有各种不同的取值 那么我们申请一个二维数组来保存中间计算的结果 |
运行结果
1 | ps = [12,3,1,3,6] |
我们看到下面的ret_mid 数据
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | None | 12 | None | 12 | None | 12 |
1 | 0 | 0 | 0 | 3 | None | None | None | 12 | None | 15 |
2 | None | 0 | None | 3 | None | None | None | 12 | None | 15 |
3 | None | None | None | 3 | None | None | None | None | None | 15 |
4 | None | None | None | None | None | None | None | None | None | 15 |
看一下依赖关系
我们根据这个图发现
第4
行的图依赖于 3
行的图数据 而第3
行的数据依赖于第2
行的数据 如果这样的话那么我们是不是可只用 两行数据来算出结果
级 i
行数据依赖于 i -1
行 , 我们可以利用滚动数组
的方法优化空间,也可以用自下而上
的优化方法来优化
滚动数组的方式 好优化 只需要 将idx % 2
就可以了
下面我来用自下而上的
来看代码
1 | # |
运行如下
1 | ps = [12,3,1,3,6] |
来看中间结果
i/v | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |
---|---|---|---|---|---|---|---|---|---|---|---|
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 0 | 0 | 0 | 0 | 0 | 12 | 12 | 12 | 12 | 12 | 12 |
2 | 0 | 0 | 0 | 0 | 3 | 12 | 12 | 12 | 12 | 15 | 15 |
3 | 0 | 0 | 0 | 0 | 3 | 12 | 12 | 12 | 12 | 15 | 15 |
4 | 0 | 0 | 3 | 3 | 3 | 12 | 12 | 15 | 15 | 15 | 15 |
5 | 0 | 0 | 3 | 3 | 3 | 12 | 12 | 15 | 15 | 15 | 15 |
总结
动态规划的主要难点就是如何划分问题,将大问题拆解为子问题之后,分析子问题和原问题的最优解,然后根据问题描述拆分通项公式,最后在分析计算的冗余度。对于逻辑分析能力是个很好的锻炼。
完全背包问题
敬请期待
赞赏一下