动态规划-自底向上的 0-1 背包问题
本文是接续上一篇文章 《自顶向下的 0-1 背包问题》 未完成的部分。超链接如下:
3 自底向上-动态规划方法求解
求解背包问题的第 3 种方法,使用动态规划。
为什么说动态规划是自底向上呢。通过前面递归的求解方法可以发现,如果想求解递归树上方节点的状态解,那就需要求解这个节点的所有子节点的解才可以得出上方节点的值。那么换一种想法,求解的过程可以直接从最底层的子节点的解开始进行。
那么会不会有这样的的疑问,如果求解任何一个节点的解都需要求它的子节点的解,这样一来难道不是要一直循环向下求解了嘛?事实上是不会的,因为在程序中有设置递归触底的条件。因为不论求哪一个节点的解都需要走到递归触底这里后,向上回溯。
这样一来问题就清晰了,使用自底向上这种思路的出发点就是从递归触底的那个状态开始向上递推。摆上状态转移方程,如下:
首先回忆下在第 2 种使用了记忆化搜索这种优化方法的 memo 数组。其实在这个数组中就可以记录这颗递归树中所有的状态。在题目中不论是背包的容量 C 还是物品所需的容量 w[i] 和 物品的数量 N,这些数值都是整数。自然 (C+1) * N 就是所有状态数量的总和了。所以说只要有一个(C+1) * N 这么多个位置便可存储所有状态的值。memo 数组刚好就是这样。为什么是 C+1 而不是 C ?这是因为容量为 0 的这种情况也是需要考虑的。
我将在这张表格中一步一步地推导每一个位置的值。表格的第一行是所有可能取值的容量,表格的第一列是所有可能考虑到的物品的 id。我将使用数据对 (0,0) 这种形式表示表格中的第 0 号行第 0 号列,也就是空白处左上角的那个位置。
定义:(i,c) 位置的定义是,在这个位置存储的值是当考虑区间为 [0,i] 的所有元素并且容量为 c 的时候背包可以达到的最大价值。
给出所有的物品编号以及它的属性,如图:
第一步:初始化
(1) 第 0 号列初始化:可以确定的是第 0 列的所有的值都为 0,因为当背包的容量为 0 的时候装不进去任何物品,自然它的价值也就为 0 了。
(2) 第 0 号行初始化:在第 0 号行的时候每一个位置都只考虑第 0 号物品。通过 "物品属性表格" 可知,0 号物品所需的容量为 1,也就是说在第一行中只有当背包容量达到 1 的时候才可以装下第 0 号物品。所以,容量为 1 之前的所有的格子中的价值都为 0, 容量为 1 的格子以及它后面的格子的价值都为第 0 号物品的价值 v[0] (因为当前只考虑第 0 号物品,所以即使背包容量再大,也不会装其余的物品)。
通过以上两步考虑便可得到如下结果:
第二步:递推第 1 号行
从推导出来的表达式来看:
(1) 当在 (1,1) 的位置时,它的取值应为 (1-1,1) 和 (1-1,1-w[1]) + v[1] 两者的最大值(分别对应着不将 1 号物品装入背包的值和将 1 号物品装入背包的值),也即 (0,1) 和 (0,-1) + v[1]。但是这是有问题的,表格中并没有 (0,-1) 对吗?即使在使用动态规划的方法的时候,在此时做分类也应当对其做检查,应该判断是否能够将这个物品装入背包中。这样一来 (1,1) 的位置是不能够考虑将 1 号物品装入背包这种情况的。那么就直接将 (0,1) 位置的值抄下来就好了。得到如下图的结果:
(2) 再来看 (1,2) 的位置,如下图:
(1,2) 位置的值可以有两种选择,如果此时将 1 号物品放入背包(背包容量满足,是可以放进去的),那么就应取 (1-1, 2-w[1]) + v[1] 也就是 (0,0) + v[1] = 10 。另外一种情况是不将 1 号物品放入背包那就应该取 (0,2) = 12 。显然 12 > 10 ,则应在 (1,2) 处更新为 12 。
(3) 同理 (1,3) 的位置:
(4) 又如 (1,4) 的位置:
第三步:递推第 2 号行
通过上面详细的分析,在这里我将直接给出第 2 号行的结果。为了更清晰的看到每个数值都是通过哪些数据推导过来的,我使用不同的颜色做了区分。如下图:
编码实现-动态规划求解
我在代码中写了注释,这和上面的图片过程是一样的。在 (2,4) 位置存储的值就是最终的答案了。
dp = dynamic programming (动态规划)
N = 3
C = 4
w = [1, 2, 2]
v = [12, 10, 12]
dp = [
[-1, -1, -1, -1, -1],
[-1, -1, -1, -1, -1],
[-1, -1, -1, -1, -1]
]
# 初始化第 0 号行
for j in range(C+1):
if j >= w[0]:
dp[0][j] = v[0]
else:
dp[0][j] = 0
# 初始化第 0 号列
for i in range(N):
dp[i][0] = 0
# 输出当前网格的值
print(dp[0])
print(dp[1])
print(dp[2])
# 递推其余位置
for i in range(1, N):
for j in range(1, C+1):
# 先将 (i,j) 位置更新为不放 i 号物品情况下的值
dp[i][j] = dp[i-1][j]
# 判断是否能将 i 号物品放入当前状态的背包中
if j >= w[i]:
# 如果能放进去则取两种情况的较大值
dp[i][j] = max(dp[i][j], v[i] + dp[i-1][j-w[i]])
# 输出当前网格的值
print("最终的网格值:")
print(dp[0])
print(dp[1])
print(dp[2])
运行截图:
注:封面图片来自网络
欢迎访问我的代码仓库:(文章中的代码都在这里)
https://gitee.com/teenager_lijh
如果你喜欢的话点一下 在看 哦。
欢迎转发 wow ~。