正式开始之前回顾一下,二维0/1背包代码
// weight数组的大小 就是物品个数
for(int i = 1; i < weight.size(); i++) { // 遍历物品
for(int j = 0; j <= bagweight; j++) { // 遍历背包容量
if (j < weight[i]) dp[i][j] = dp[i - 1][j];
else dp[i][j] = max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
0/1背包的遍历顺序
首先,总结写0/1背包遍历循环的关键:物品只能被放入一次背包,不能被多次放入背包。放在递推表达式上看,就是每次更新
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
在计算dp[j]的时候,不能重复使用 放入同一个物品 而计算出的旧dp[j]。
1、一维dp数组如何初始化
关于初始化,一定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱。
dp[j]表示:容量为j的背包,所背的物品价值可以最大为dp[j],那么dp[0]就应该是0,因为背包容量为0所背的物品的最大价值就是0。
那么dp数组除了下标0的位置,初始为0,其他下标应该初始化多少呢?
看一下递归公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
dp数组在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了。
这样才能让dp数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了。
那么我假设物品价值都是大于0的,所以dp数组初始化的时候,都初始为0就可以了。
- 一维dp数组遍历顺序
代码如下:
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
这里大家发现和二维dp的写法中,遍历背包的顺序是不一样的!
二维dp遍历的时候,背包容量是从小到大,而一维dp遍历的时候,背包是从大到小。
为什么呢?
倒序遍历是为了保证物品i只被放入一次!。但如果一旦正序遍历了,那么物品0就会被重复加入多次!
举一个例子:物品0的重量weight[0] = 1,价值value[0] = 15
如果正序遍历
dp[1] = max(0,dp[1 - weight[0]] + value[0]) = 15
dp[2] = max(0,dp[2 - weight[0]] + value[0]) = 30
此时dp[2]就已经是30了,意味着物品0,被放入了两次,所以不能正序遍历。
为什么倒序遍历,就可以保证物品只放入一次呢?
倒序就是先算dp[2]
dp[2] = dp[2 - weight[0]] + value[0] = 15 (dp数组已经都初始化为0)
dp[1] = dp[1 - weight[0]] + value[0] = 15
所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。
那么问题又来了,为什么二维dp数组遍历的时候不用倒序呢
因为对于二维dp,dp[i][j]都是通过上一层即dp[i - 1][j]计算而来,本层的dp[i][j]并不会被覆盖!
(如何这里读不懂,大家就要动手试一试了,空想还是不靠谱的,实践出真知!)
再来看看两个嵌套for循环的顺序,代码中是先遍历物品嵌套遍历背包容量,那可不可以先遍历背包容量嵌套遍历物品呢?
不可以!
因为一维dp的写法,背包容量一定是要倒序遍历(原因上面已经讲了),如果遍历背包容量放在上一层,那么每个dp[j]就只会放入一个物品,即:背包里只放入了一个物品。
代码一:先倒序遍历背包 再正序遍历物品
for (int j = 10; j >= 0; j--) { // 遍历背包容量
for (int i = 0; i < weight.size(); i++) { // 遍历物品
if (j >= weight[i]) {
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
}
想象一下,也就是在x轴代表容量,y轴代表物品的二维表格遍历。
当dp[10] 确实不会被之前的dp[j]影响,但同时dp[10]只会放入一个价值最大的物品。
而小于10的j dp[j]可能受到前面计算出来的dp[j]影响 比如之前算出来的dp[7]大于 j=7这一轮的dp[7],但是也只会放入一件物品,那就是只装入之前weight=3的物品
最后的结果就是背包只放入了一个价值最大的物品。
或者是本轮循环遍历j的时候,dp[j - weight[i]] + value[i]比之前算的dp[j]大那么就会改装下这一轮价值最大的物品。
放反正遍历完后背包只装了一件物品。
代码二:先正序遍历背包 再正序遍历物品
for (int j = 0; j <= 10; j++) { // 遍历背包容量
for (int i = 0; i < weight.size(); i++) { // 遍历物品
if (j >= weight[i]) {
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
}
}
}
那么这样后续的dp[j]我使用前面行dp[j]的值,比如如果i重量最小,价值最大,
比如weight[i] = 1, value = 10,
dp[9] = max(dp[9], dp[8] + 10)
dp[10] = max(dp[10], dp[9] + 10)
那么每一次dp[j]都会加入i物品,最后导致同一物品被加入很多次。
所以一维dp数组的背包在遍历顺序上和二维其实是有很大差异的!,这一点大家一定要注意。