49.01背包问题

前言

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个物品最佳组合对应的最大价值

根据第一步中ij表示的含义,可以有两个方向来推出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;
    }
}
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值