前言
01背包问题在力扣中没有直接给出,而是需要将一些问题转换成01背包问题去解决,所以本篇文章专门讲解一下01背包问题,这不属于力扣中的题目,算是知识扩展。
一、问题描述
有N件物品和一个容量为V的背包。第i件物品的重量是w[i],价值是v[i]。求解将哪些物品装入背包可使这些物品的重量总和不超过背包容量,且价值总和最大。
这里举一个例子:
背包最大重量为4, 物品为:
问背包能背的物品最大价值是多少?
二、解题思路及代码演示
解题思路一
依然使用动规五部曲:
第一步:确定dp数组以及下标的含义
对于背包问题,有⼀种写法, 是使⽤⼆维数组,即dp[i][j]
表示从下标为[0-i]的物品⾥任意取,放进容量
为j的背包,价值总和最⼤是多少。
第二步:确定递推公式
为了能够更好的理解01背包问题,这里定义几个变量,value(i)表示的是第i个物品的价值,W(i)表示第i个物品的体积,定义value(i, j)表示当前背包容量j,前i个物品最佳组合对应的最大价值
根据第一步中i
和j
表示的含义,可以有两个方向来推出dp[i][j]
,就是第i个物品放或是不放:
- 由
dp[i-1][j]
推出,即背包容量为j,⾥⾯不放物品i,此时dp[i][j]
就是dp[i - 1][j]
- 由
dp[i-1][j-weight[i]]
推出,dp[i-1][j-weight[i]]
为背包容量为j-weight[i]
的时候不放物品i,那么dp[i-1][j-weight[i]]+value[i]
(物品i的价值),就是背包放物品i得到的最⼤价值.
好好理解一下这两个条件,其实就是第i个物品装不装的问题,一定要记住dp[i][j]
表示的是最大价值,
第一个条件(不装)是说,我的背包容量是j,当选择某一个物品i时,这个物品i的体积比背包剩余的体积大,装不下,但是这时i所对应的的价值与前i-1个是一样的,就不装了,所以就有dp[i][j]=dp[i-1][j]
第二个条件(装)是,背包总的容量是j,有足够的的空间放物品i,现在我不放第i个物品,那么背包里剩下的物品容量是j-weight[i],也就是此时最大价值是dp[i-1][j-weight[i]]
,这是放入物品i之前的状态,我现在要想得到最优的就有当前价值加上i的价值,dp[i-1][j-weight[i]]+value[i]
。
所以递推公式:dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i])
第三步:dp数组初始化
关于初始化,⼀定要和dp数组的定义吻合,否则到递推公式的时候就会越来越乱。
⾸先从dp[i][j]
的定义触发,如果背包容量j为0的话,即价值dp[i][0]
,⽆论是选取哪些物品,背包价值总和⼀定为0。如图:
状态转移⽅程 dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i])
; 可以看出i 是由 i-1 推导出来,那么i为0的时候就⼀定要初始化。
dp[0][j]
,即:i为0,存放编号0的物品的时候,当前背包所能存放的物品最⼤价值。
代码如下:
for (int j = bagWeight; j >= weight[0]; j--) {
dp[0][j] = dp[0][j - weight[0]] + value[0]; // 初始化i为0时候的情况
}
这个初始化使用的是倒叙遍历,那为什么不用正序遍历呢?下面来分析一下
dp[0][j]
表示容量为j的背包存放物品0时候的最⼤价值,物品0的价值就是15,因为题⽬中说了每个物品只有⼀个!所以dp[0][j]
如果不是初始值的话,就应该都是物品0的价值,也就是15。
但如果⼀旦正序遍历了,那么物品0就会被重复加⼊多次! 例如代码如下:
for(int j=weight[0]; j<=bagweight; j++){
dp[0][j] = dp[0][j-weight[0]] + value[0];
}
可以来看一下这个正序遍历,例如dp[0][1]
是15,到了dp[0][2] = dp[0][2-1] + 15
; 也就是dp[0][2] = 30
了,那么就是物品0被重复放⼊了。所以这里是不能使用正序遍历的。所以必须使用倒叙遍历,确保每个物品纸杯放了一次。此时数组初始化情况如下:
dp[0][j]
和dp[i][0]
都已经初始化了,dp[i][j]在推导的时候⼀定是取价值最⼤的数,如果题⽬给的价值都是正整数那么⾮0下标都初始化为0就可以了,因为0就是最⼩的了,不会影响取最⼤价值的结果。如果题⽬给的价值有负数,那么⾮0下标就要初始化为负⽆穷了。例如:⼀个物品的价值是-2,但对应的位置依然初始化为0,那么取最⼤值的时候,就会取0⽽不是-2了,所以要初始化为负⽆穷。
最后初始化代码如下:
int[][] dp = new int[weight.length+1][bagweught+1]
for (int j = bagWeight; j >= weight[0]; j--) {
dp[0][j] = dp[0][j - weight[0]] + value[0];
}
第四步:确定遍历顺序
有两个遍历的维度:物品与背包重量:
其实先遍历那个都是可以的,下面先来遍历物品来看看:
for(int i = 1; i < weight.length(); i++) { // 遍历物品
for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
if (j < weight[i]){
dp[i][j] = dp[i-1][j]; // 这个是为了展现dp数组⾥元素的变化
}else{
dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]);
}
}
}
下面是先遍历背包容量:
// weight数组的⼤⼩ 就是物品个数
for(int j = 0; j <= bagWeight; j++) { // 遍历背包容量
for(int i = 1; i < weight.size(); i++) { // 遍历物品
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]);
}
}
}
第五步:举例推导dp数组
来看⼀下对应的dp数组的数值,如图:
最终结果就是dp[2
][4]。
代码演示一
package BackPack;
public class main {
public static void main(String[] args) {
//背包最大容量
int bagWeight = 4;
//表示每物品的重量
int weight[] = {1, 3, 4};
//每个物品的价值
int value[] = {15, 20, 30};
//调用方法
int dp[][] = BackPack_Solution(bagWeight, weight, value);
//打印部分
for (int i = 1; i<=weight.length; i++) {
for (int j = 0; j<=bagWeight; j++) {
System.out.print(dp[i-1][j]+"\t");
if(j==bagWeight){
System.out.println();
}
}
}
}
public static int[][] BackPack_Solution(int bagWeight, int[] weight, int[] value) {
//dp[i][j]表示前i件物品恰放入一个重量为m的背包可以获得的最大价值
int dp[][] = new int[weight.length+1][bagWeight+1];
//dp数组的初始化
for (int j = bagWeight; j >= weight[0]; j--) {
dp[0][j] = dp[0][j-weight[0]] + value[0];
}
//遍历部分
for (int i = 1; i < weight.length; i++) {
for (int j = 0; j <= bagWeight; j++) {
if (j < weight[i]) {
dp[i][j] = dp[i-1][j];
}else{
dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i]);
}
}
}
return dp;
}
}
结果截图:
解题思路二
(滚动数组)
其实滚动数组就是将二维dp降为一维dp,下面将详细介绍一下。
在使⽤⼆维数组的时候,递推公式:dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i])
;
其实可以发现如果把dp[i-1]那⼀层拷⻉到dp[i]上,表达式完全可以是:dp[i][j] = max(dp[i][j], dp[i][j- weight[i]] + value[i])
;
于其把dp[i-1]这⼀层拷⻉到dp[i]上,不如只⽤⼀个⼀维数组了,只⽤dp[j](⼀维数组,也可以理解是⼀个滚动数组)。
这就是滚动数组的由来,需要满⾜的条件是上⼀层可以重复利⽤,直接拷⻉到当前层。
读到这⾥估计⼤家都忘了 dp[i][j]
⾥的i和j表达的是什么了,i是物品,j是背包容量。
dp[i][j]
表示从下标为[0-i]的物品⾥任意取,放进容量为j的背包,价值总和最⼤是多少。
⼀定要时刻记住这⾥i和j的含义,要不然很容易看懵了。
依然使用五个步骤:
第一步:确定dp数组的定义
在⼀维dp数组中,dp[j]表示:容量为j的背包,所背的物品价值可以最⼤为dp[j]。
第二步:递推公式
dp[j]为 容量为j的背包所背的最⼤价值,那么如何推导dp[j]呢?
dp[j]
可以通过dp[j - weight[j]]
推导出来,dp[j - weight[i]]
表示容量为j - weight[i]
的背包所背的最⼤价
值。
dp[j - weight[i]] + value[i]
表示 容量为 j - 物品i重量 的背包 加上 物品i的价值。(也就是容量为j的背
包,放⼊物品i了之后的价值即:dp[j])
此时dp[j]有两个选择,⼀个是取⾃⼰dp[j],⼀个是取dp[j - weight[i]] + value[i],指定是取最⼤的,毕
竟是求最⼤价值,
所以递归公式为:
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
第三步: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就
可以了,如果题⽬给的价值有负数,那么⾮0下标就要初始化为负⽆穷。
这样才能让dp数组在递归公式的过程中取的最⼤的价值,⽽不是被初始值覆盖了。
那么我假设物品价值都是⼤于0的,所以dp数组初始化的时候,都初始为0就可以了
第四步:确定遍历顺序
for (int i = 1; i < weight.length; i++) {
for (int j = bagWeight; j >= weight[i]; j--) { //注意这里是倒叙
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
}
注意这里是倒叙,这个地方与二维遍历是有区别的。还需要注意一点就是,这里的for循环是先遍历的物品再遍历的背包容量,是不可以反过来的。,下面详细分析一下为什么是倒叙遍历呢?
先来对比一下两个式子:
二维递推式:dp[i][j] =Math.max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i])
一维递推式:dp[j] =Math.max(dp[j], dp[j-weight[i]] + value[i]);
可以发现, 在一维递归式里, 要求dp[j-weight[i]] + value[i]
这部分 代替 dp[i-1][j-weight[i]] + value[i]
这部分
我们现在又只有一维数组,这就要保证, 在第i次外循环时, 调用的dp[j-weight[i]]
实际上是基于第i-1
次循环得到的值。
而逆序保证了, 对于dp[j]
, 它要调用的dp[j-weight[i]]
一定是第i层循环还没有更新过的, 换言之, dp[j-weight[i]]
只有可能是第i-1层存储的数据.
比如说,这里举一个例子,背包容量是bagWeight=4,
我们上面的例子, 内层循环从bagWeight=4开始往下减, 第一个数就是求dp[4] =Math.max(dp[4], dp[4-weight[i]] + value[i])
这时我们要知道的dp[4-weight[i]]
其实是二维里的dp[i-1][10-weight[i]]
那么我内层循环才从4开始, 才第一次啊!所以内层循环是根本不会去更新dp[10-weight[i]]这个数, 也就是上面说的调用的
dp[j-weight[i]]一定是第i层循环还没有更新过的
,那么这里面存储的是什么玩意呢?
肯定是第i-1次外循环过一遍存储的结果,比如下面当i=1时,内层循环刚开始时,dp[j-weight[i]] = dp[4-3] = 15
,这个15就是外循环初始的dp = [0,15,15,15,15]
中的dp[4-3] = dp[1]。
文字理解起来有点困难(对于我来说),通过把最上面的例子结合代码一步一步推导一下:
初始:bagWeight = 4;
weight = [1,3,4];
value = [15,20,30];
dp = [0,15,15,15,15]; //选择第1个物品时,背包中现在的价值
value[0] = 15;
weight[0] = 1
外循环第一轮:当i = 1时,
内层循环第一轮:j = bagWeight = 4
变化的量:
weight[i] = 3; //外层循环的i
dp[j] = dp[4] = 15;
dp[j-weight[i]] = dp[4-3] = 15;
value[i] = value[1] = 20; //第二个(下标为1)物品的价值
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i])
= dp[4] = Math.max(15, 15 + 20)
=35
此时 dp = [0,15,15,15,35]
内层循环第二轮:j = 3
变化的量:
weight[i] = 3; //还是外层循环的i
dp[j] = dp[3] = 15;
dp[j-weight[i]] = dp[3-3] = 0;
value[i] = value[1] = 20; //还是外层循环的i
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i])
= dp[3] = Math.max(15, 0 + 20)
=20
此时 dp = [0,15,15,20,35]
内层循环第三轮:j = 2 < weight[i]=3,所以内层循环结束
外循环第二轮:当i = 2时,
内层循环第一轮:j = bagWeight = 4
变化的量:
weight[i] = 4; //外层循环的i
dp[j] = dp[4] = 35;
dp[j-weight[i]] = dp[4-4] = 0;
value[i] = value[2] = 30; //第3个(下标为2)物品的价值
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i])
= dp[4] = Math.max(35, 0 + 20)
=35
此时 dp = [0,15,15,20,35]
内层第二轮循环:j=4 >= weight[i]=4,内层for循环结束
外循环第三轮:当i=3时,i<weight.length条件不成立。此时for循环终止返回的dp = [0,15,15,20,35]
那如果内层循环的正序遍历,是不能够保证每个物品只取一次。如下
dp[1] = dp[1 - weight[0]] + value[0] = 15
dp[2] = dp[2 - weight[0]] + value[0] = 30
此时dp[2]就已经是30了,意味着物品0,被放⼊了两次,所以不能正序遍历。
第五步:举例推导dp数组
⼀维dp,分别⽤物品0,物品1,物品2 来遍历背包,最终得到结果如下:
代码演示二
package BackPack;
public class main_3 {
public static void main(String[] args) {
//背包最大容量
int bagWeight = 4;
//表示每物品的重量
int weight[] = {1, 3, 4};
//每个物品的价值
int value[] = {15, 20, 30};
//调用方法
int dp[] = BackPack_Solution(bagWeight, weight, value);
//打印部分
for (int i = 1; i <= weight.length; i++) {
for (int j = 0; j <= bagWeight; j++) {
System.out.print(dp[j] + "\t");
if (j == bagWeight) {
System.out.println();
}
}
}
}
public static int[] BackPack_Solution(int bagWeight, int[] weight, int[] value) {
//dp[i][j]表示前i件物品恰放入一个重量为m的背包可以获得的最大价值
int dp[] = new int[bagWeight + 1];
//dp数组的初始化
for (int j = bagWeight; j >= weight[0]; j--) {
dp[j] = dp[j - weight[0]] + value[0];
}
//遍历部分
for (int i = 1; i < weight.length; i++) {
for (int j = bagWeight; j >= weight[i]; j--) { //注意这里是倒叙
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
}
}
return dp;
}
}