文章目录
- 什么是01背包问题?
- 二维
- 动态规划五步走:
- 一维 (滚动数组)
什么是01背包问题?
有很多种背包 ,完全背包啊概率背包啊,多重背包啥的,但都逃不过01背包,01背包是基础、
题目大概是这样的:
有一个小偷,背着书包去偷东西,这个小偷的背包有固定容量,所偷的物品有固定的价值和其对应的重量。
现在问,小偷能偷走东西的最大价值。
二维
老规矩
动态规划五步走:
确定dp数组以及下标的含义:
dp[i][j] 表示从1到i中任意选择物品,背包容量为j时的最大价值。这里选择物品可以是1件,可以是2件,也可以是i-1件全拿。
可以看下面这个我偷来的图帮助理解。
2.确定递推公式:
动态规划思想都是局部最优,然后一步一步的最优推到最后结果。
细致化的看这个问题,对于一件物品,只有两种可能,小偷选择偷走,或者不偷。
- 不偷:则背包的容量没变化,偷走物品的总价值也没变化,那么此时dp[i][j] = dp[i-1][j] 。可以看上面那个二维数组的图,该偷x号物品了,你选择不偷,那你此时的背包容量和总价值就和上一个物品时的容量和总价值一样,所以就是dp[i][j] = dp[i-1][j] ,对应上面的图就是上面的格子移动到下面了,数值没变。
- 偷:此时背包容量需要减去即将放进来的这件物品的容量,总价值也要加上即将放进来的这件物品的价值,所以dp[i][j] = dp[i-1][j-weight[i]] + value[i],这个递推公式可以这样理解,假如现在背包容量为10,即将放进来的物品重量为3,那么这时候需要找到背包容量为10-3 = 7,时的最大价值,再加上此时的物品的价值,就是新的背包容量10时的最大价值。
所以递推公式: dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
3.dp数组初始化:
看一下上面的递推公式,想要得到某个格子的值,要么由其正上方的格子推过来(不偷的情况),要么由左上角的区域推过(偷的情况)。
所以初始化需要最上面一行和最左边一列。
再看一下我偷的这个图。
显然,当背包容量为0的时候,装不下任何东西,所以最左边一列都是0。
最上面一行就要判断物品0的重量了,小于背包容量则填价值,否则填0。
其余地方初始化什么都一样,因为其他的地方都是由上面或者左上推过来的,都会被计算新的值然后覆盖掉。
4.确定遍历顺序:
这个其实也无所谓,因为刚才说了除了第一列和第一行之外的地方都是新计算出来的,所以无论先遍历物品还是先遍历背包都可以。
一般情况下都是先遍历物品,再遍历背包。
5.举例推导dp数组:
这一步其实就是遇到真实题的时候,如果遇到错误,可以直接按照推导公式模拟一边结果,便于debug。
一维 (滚动数组)
一维比二维的难理解感觉。
首先再看一下二维的那张图
二维变一维,但是for循环依旧是两层,可以理解成,每次都是一个新的一维数组,也就是每次新的一维数组依赖于上一次的一维数组做覆盖。
看最上面的图,也就每次都是新的一行,依赖于上一行。
这一点可以从二维的递推公式中看出来:dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
无论是哪一种情况,拿或者不拿,都是依赖于上一行的状态。
所以着也叫滚动数组,就是一行一行整体往下滚动嘛。
难点:
很多题解在讲遍历顺序的时候都会说要倒叙遍历,说什么01背包每件物品只能拿一次,正序遍历就拿了两次,巴拉巴拉一堆。
这块具体为啥要倒叙遍历呢?
首先要明白二维数组的递推过程,然后才能看懂二维变一维的过程。
假设目前有背包容量为10,可以装的最大价值, 记为g(10)。
即将进来的物品重量为6。价值为9。
那么此时可以选择装该物品或者不装该物品。
如果不装该物品,显然背包容量无变化,这里对应二维数组,其实就是取该格子上方的格子复制下来,就是所说的滚动下来,直接g[10] = g[10],这两个g[10]要搞清楚,右边的g[10]是上一轮记录的,也就是对应二维数组里上一层的值,而左边是新的g[10],也就是对应二维数组里下一层的值。
如果装该物品,则背包容量= g(10-6) = g(4) + 9 ,也就是 g(10) = g(4) + 6 ,这里的6显然就是新进来的物品的价值,g(10)就是新记录的,对应二维数组里下一层的值,而这里的g(4)是对应二维数组里上一层的值,通俗的来讲:你要找到上一层也就是上一状态下 背包容量为4时的能装的最大价值,用它来更新下一层的这一状态,也就是加入了价值为9的物品的新状态。
这时候如果是正序遍历会怎么样? g(10) = g(4) + 6 ,这个式子里的g(4)就不再是上一层的了,因为你是正序啊,g(4) 比g(10)提前更新,那么此时程序已经没法读取到上一层的g(4)了,新更新的下一层的g(4)覆盖掉了,这里也就是为啥有题解说一件物品被拿了两次的原因。
遍历的时候先遍历物品,再背包,不能颠倒:
# 先遍历物品, 再遍历背包容量
for i in range(len(weight)):
for j in range(bag_weight, weight[i] - 1, -1):
# 递归公式
dp[j] = max(dp[j], dp[j - weight[i]] + value[i])
参考1
参考2