前言
转眼,我们已经接触了动态规划三天了。从一开始的萌新小白,到会使用五步法解决一些动态规划的经典例题了。今天开始,我们要开始的内容,可谓是经典得不能再经典的背包问题,话不多说我们直接进入正题。
背包问题基础
常见的背包问题有三种:01背包、完全背包还有多重背包。我们要掌握的是01背包和完全背包,多重背包可做拓展了解。看到这里不少人还是懵逼的状态吧,没关系,我也是( ﹁ ﹁ ) ~→。
怎么区分这三种背包呢,或者这三种背包是什么意思呢?Carl哥帮我们做了分析。
所谓背包问题:简言之就是假设你有一个背包,然后有一堆物品,你背包容量是有限的,面对到一个物品时,你是选择把它装进背包或者不装进背包的问题,怎么使得价值最大化,是背包问题通常所考虑的。
我们接下啦三天左右的学习,都是针对背包问题展开,今天讨论01背包。
01背包理论基础
根据上面的图中所述:01背包是只有一个对应物品(有价值、体积),你选择与否对背包价值影响走向问题。
下面参考代码随想录的例子进行讲解。
假设背包容量为4,物品为:
重量 | 价值 | |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
我们应该如何选择保证背包在不溢出的情况下得到价值量最大的物品呢?
因为数据量太少,人眼看过去是很容易看出来的,但是这是为了方便理解,计算机需要知道怎么执行才是关键!
下面我使用两种常见的方法来解决此问题
二维dp数组背包
既然是二维数组,那dp肯定初始化的结构是:dp[i][j]
按照五步法的思维去解决这道题目
1.理解数组dp[i][j]的含义
毋庸置疑,dp数组的含义是价值量也正是我们要求得的,那i和j的含义是什么呢?这里给出解释:i是选择到当前第i个物品,j是指选择到当前物品时背包的容量。
2.确定递推公式:
再回顾一下下dp[i][j]的含义:从下标为[0-i]的物品⾥任意取,放进容量为j的背包,价值总和最⼤是多少。
那么有两个方向可以推导出dp[i][j]
- 由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得到的最⼤价值。
然后我们不难推出动态转移方程:
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][[j-weight[i]]+value[i])
3.确定dp数组的初始化
关于初始化一定要和前面动态转移方程能相互对应,我们还是要理解dp[i][j]
首先,如果j为0,即意为背包容量为0,你无论怎么样都无法装入物品价值一定为0。
再想想对于物品0,如果有背包重量时,存放物品0对应的价值应该是如何初始化?
我们知道物品0的重量(体积)是1,价值是15,所以有背包容量后它的最高价值都会有15,理解是理解了,但是如何初始化它又成为一个问题。
我们采用倒序遍历(为什么不采用正序遍历?)
// 倒叙遍历
for (int j = bagWeight; j >= weight[0]; j--) {
dp[0][j] = dp[0][j - weight[0]] + value[0]; // 初始化i为0时候的情况
}
这里再贴出正序遍历的代码,uu们可以自己去初始化遍历一下为什么不行
// 正序遍历
for (int j = weight[0]; j <= bagWeight; j++) {
dp[0][j] = dp[0][j - weight[0]] + value[0];
}
是的,你一定发现了如果正序遍历前面的数组会对后面的数组进行累加,这里就相当于物品被重复放入了!而我们知道0,1背包的物品是只有1个的,所以一定要使用倒叙遍历,保证物品0只被放入一次!
初始化后的数组如图所示:
至于其它位置的值初始化,我认为若价值不是负数的话初始化为0就行了,Carl哥的思想是若一个物品价值是负数,将它初始化为-∞,我认为让它保持原数就可以了。
4.确定遍历顺序
由于dp数组有两个维度:物品与背包容量
所以我们可以选择从物品开始遍历,也可以选择优先遍历背包容量,按照理解难度的话,先遍历物品是更好理解的
// 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]; // 这个是为了展现dp数组⾥元素的
变化
else dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weight[i]] + value[i]);
}
}
5.控制台打印
打印输出的表应该是如下结构的:
完整的Java版代码我也写在这里,有兴趣的uu们可以去编译器中打印试着输出理解一下。
package demo18;
public class bagValue {
public static void main(String[] args) {
int[] weights=new int[]{1,3,4}; //重量(体积)
int[] values=new int[]{15,20,30}; //价值
int bagWeights=4; //背包容量
int n=weights.length;
//创建dp数组
int[][] dp=new int[n][bagWeights+1];
//初始化dp数组(先初始化背包容量为0的情况)
for (int i = 0; i < n; i++) {
dp[i][0]=0;
}
//初始化dp数组(然后初始化背包容量1-4时是否放入物品0的最高价值)
for(int j=bagWeights;j>=weights[0];j--){
dp[0][j]=dp[0][j-weights[0]]+values[0];
}
//遍历dp数组,按照物品优先的遍历顺序,然后按照背包容量进行排序
for(int i=1;i<n;i++){ //按照物品进行遍历
for(int j=0;j<=bagWeights;j++){ //按照背包遍历
if(j>=weights[i]) //背包容量需要大于当前物品容量
dp[i][j]=Math.max(dp[i-1][j],dp[i-1][j-weights[i]]+values[i]);
else
dp[i][j]=dp[i-1][j]; //无法选择,那么就继承上种情况的最大值
}
}
//打印输出查看是否和预期一致
for (int i = 0; i < n; i++) {
for (int j = 0; j <=bagWeights ; j++) {
System.out.printf("%d ",dp[i][j]);
}
//换行
System.out.println();
}
}
}
打印结果如下:
看来是与预期是一致的,证明这种思路是可行的!
滚动数组背包
有些细心的uu们就会产生上一种使用二维数组解dp问题的过程的一些疑问:
我们是创建了一个二维数组dp[i][j]来代表当前价值最大值,i是指遍历到第i个物品,而j是指当前背包的容量。但是,好像i是从0遍历到i-1,即它好像不存在什么意义?仅作为一个标志让我们更好理解!
下面我们依旧通过这个例子来使用一维dp滚动数组进行讲解
重量 | 价值 | |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
这里给出一个滚动数组需要满足的条件上⼀层可以重复利用
这里依旧采用五步法来解决此问题:
1.确定dp数组的含义:
dp[j]表示:容量为j的背包,所背的背包最大价值为dp[j]
2.确定动态转移方程:
如何确定dp[j]的最大价值呢?对于一个物品,我们可以选择(放/不放)两种选择
选择不放的话,那么dp[j]是继承了上一个状态的背包容量,即背包容量不会减少;如果选择放的话,那么dp[j]是会减少相应的背包容量:dp[j]=dp[[j-weights[i]]+values[i]
所以可以得出以下动态转移方程:
dp[j]=Math.max(dp[j],dp[[j-weights[i]]+values[i])
可以看出相对于⼆维dp数组的写法,就是把dp[i][j]中i的维度去掉了!
3.确定dp数组的初始化:
这一步尤为重要:起点决定终点,⼀定要和dp数组的定义吻合。
对于这一步,我们只需要清楚:若背包容量为0时,dp[0]是一定为0的,至于存在背包容量后,我们不确定是否能放入物品,所以价值都初始化为0就好【若存在负价值的物品,不选即是最佳,因此也默认为0】
根据上面论述,dp数组全部初始化为0动作干净又利索。
4.确定遍历顺序:
我们想象以下放物品的时候,背包一开始是不是空的,此时容量最大,然后不断选择放入物品进去,容量越来越小,所以我们选择这样的方式进行遍历是不是会更方便呢?
所以确定遍历的顺序是这样的:
for(int i = 0; i < weight.size(); i++) { // 遍历物品
for(int j = bagWeight; j >= weight[i]; j--) { // 遍历背包容量
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
}
}
部分同学可能会有以下的疑惑:
5.控制台打印输出
这里我依然给出源代码并打印输出相应信息,读者可以自行尝试
package demo18;
public class bagValue2 {
public static void main(String[] args) {
int[] weights=new int[]{4,3,1}; //重量(体积)
int[] values=new int[]{30,20,15}; //价值
int bagWeights=4; //背包容量
int n=weights.length;
//创建dp数组
int[] dp=new int[bagWeights+1];
//初始化dp数组:由于数组都是初始化为0,所以可以不予操作
//动态转移方程写出所有情况
for(int i=0;i<n;i++){
for (int j = bagWeights; j >=weights[i] ; j--) {
if(j-weights[i]>=0)
dp[j]=Math.max(dp[j],dp[j-weights[i]]+values[i]);
}
}
//控制台打印输出
for (int i = 0; i < bagWeights+1; i++) {
System.out.printf("%d ",dp[i]);
}
}
}
总结
今天我们对动态规划的背包问题做了介绍并且学习了:01背包的两种解法,两点几啦,饮茶先啦!