代码随想录刷题记录day37 0-1背包+分割等和子集
0-1背包
问题:有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
例题:
背包最大重量为4。
物品为:
重量 | 价值 | |
---|---|---|
物品0 | 1 | 15 |
物品1 | 3 | 20 |
物品2 | 4 | 30 |
问背包能背的物品最大价值是多少?
分析:
- 二维数组
动态规划五步曲
- 确定dp数组的含义
dp[i][j] 表示从下标为0-i的物品随便取,放进容量为j的背包的最大的价值。
可以有两个方向推导过来。
不放物品i:
dp[i][j]=dp[i-1][j],表示从0到i-1中任意选取,放到容量为j的背包的最大的价值。
放物品i
dp[i][j]=dp[i-1][j-weight[i]]+value[i]。表示从0到i-1中任意选,放到容量为j-weight[i]的背包,加上第i个的价值
-
确定递推公式
由上可得
dp[i][j]=max(dp[i-1][j] , dp[i-1][j-weight[i]]+value[i])
-
初始化
dp[i][0],背包容量为0得时候都是0,
又i是由i-1状态所得的,所以需要对dp[0][j]初始化,dp[0][j]表示背包容量为j,只有物品0可装的最大价值。
所以当容量j大于物品0所需的容量时,dp[0][j]=value[0],
否则dp[0][j]=0
初始化代码如下所示
for (int j = 0 ; j < weight[0]; j++) { //j<weight[0] 表示背包容量j小于物品0的重量 dp[0][j] = 0; } // 正序遍历 for (int j = weight[0]; j <= bagweight; j++) { dp[0][j] = value[0]; }
初始化后的二维数组
-
确定遍历顺序
先遍历背包顺序还是先遍历物品的顺序呢?二维dp数组都是可以的
比如先遍历物品顺序
for(int i=1;i<weight.length;i++){ for(int j=1;j<=bagsize;j++){ if (j < weight[i]) dp[i][j] = dp[i - 1][j]; //如果背包的容量小于物品i的重量, else dp[i][j]=Math.max(dp[i-1][j] , dp[i-1][j-//从物品数量开始遍历 物品从1-2 物品1已经初始化过了 为什么要初始化 因为 动态规划方程中 用到了 i-1 所以物品0一开始的时候就要初始化 // 背包重量从1-4 如果背包重量是0的话 价值就为0了 已经初始过了 就不用再继续初始化了 for (int i = 1; i < weight.length; i++) { for (int j = 1; j < bagsize + 1; j++) { //如果容量小于当前物品的重量 也就是说当前i号物品 是放不下背包的 必须得加上这一步的判断 因为j-weight[i]可能为负数 if(j<weight[i]) dp[i][j]=dp[i-1][j]; else dp[i][j]=Math.max(dp[i-1][j-weight[i]]+value[i],dp[i-1][j]);//判断是把这个物品放进去价值大 还是不放进去价值大 } }weight[i]]+value[i]) } }
先遍历背包容量也可,因为所需要的二维数组是由上方和左边推导出来的。
5.打印数组
-
一维数组
也叫做滚动数组,当前数组的值依赖于上一个数组
动态规划五步曲:
-
一维数组dp[j]的定义
表示背包容量为j,能装下最大的价值。
-
递推公式
dp[j]可由dp[j-weight[i]]推导而来
dp[j-weight[i]]表示背包容量为j-weight[i]能放下的最大的价值
dp[j]的选择有两个
- 一个是装物品i,dp[j]=dp[j - weight[i]] + value[i]
- 一个是不装物品,dp[j]=dp[j]
所以递推公式
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
-
初始化
dp[0]=0表示背包容量为0的能放下最大的价值就是0
dp数组在推导的时候一定是取价值最大的数,如果题目给的价值都是正整数那么非0下标都初始化为0就可以了。
这样才能让dp数组在递归公式的过程中取的最大的价值,而不是被初始值覆盖了。
-
遍历顺序
先遍历物品,再遍历背包,且背包需要从后往前遍历
for (int i = 0; i < wLen; i++){ for (int j = bagWeight; j >= weight[i]; j--){ dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]); } }
首先说明为什么倒叙遍历
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
举一个例子:物品0的重量weight[0] = 1,价值value[0] = 15
如果正序遍历
dp[1] = dp[1 - weight[0]] + value[0] = 15
dp[2] = 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
所以从后往前循环,每次取得状态不会和之前取得状态重合,这样每种物品就只取一次了。
为什么先遍历背包再遍历物品不行
如果先遍历背包在遍历物品
for (int j = bagsize; j >= 1; j--) { for (int i = 0; i < wLen; i++) { if (j < weight[i]) dp[j] = dp[j]; else dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]); } }
只会选择一个物品,因为背包倒叙遍历的原因,前面的还没有初始化,dp数组为 0 15 15 20 30
-
dp数组打印
0 15 15 20 35
代码
public static void testBagProblem2(int [] weight ,int[] value, int bagsize){
//初始化二维数组 一共有3个物品
int wLen = weight.length;
//定义dp数组:dp[j]表示背包容量为j时,能获得的最大价值
int[] dp = new int[bagsize + 1];
//遍历顺序:先遍历物品,再遍历背包容量
for (int i = 0; i < wLen; i++){
for (int j = bagsize; j >= weight[i]; j--){
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
}
}
// for (int j = bagsize; j >= 1; j--) {
// for (int i = 0; i < wLen; i++) {
// if (j < weight[i]) dp[j] = dp[j];
// else
// dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
// }
// }
//打印dp数组
for (int j = 0; j <= bagsize; j++){
System.out.print(dp[j] + " ");
}
}
416. 分割等和子集
思想
套用01背包
背包的容量:sum/2
物品 nums,其中nums中的数值既表示重量又表示价值。
递归五步曲
-
dp数组的含义
dp[j] 表示容量为j的背包,能装下的最大的价值
-
递推公式
dp[j]=max(dp[j],dp[j-weight[i]]+value[i]) 加物品i和不加物品i
dp[j]=max(dp[j],dp[j-nums[i]]+nums[i])
nums[i] 既表示物品i的重量也表示物品i的价值
-
初始化
dp[0]=0;因为有取最大值,所以其他的也取为0
-
遍历顺序,先遍历物品在遍历背包
for(int i=0;i<nums.length;i++){//先遍历物品
for(int j=target;j>=nums[i];j–){//为什么j>=nums[i] 因为当j<nums[i]时,不可能放物品 dp[j]=0;
dp[j]=Math.max(dp[j],dp[j-nums[i]]+nums[i]);
}
} -
打印数组
nums = [1,5,11,5] 下标 0 1 2 3 4 5 6 7 8 9 10 11 dp= {0,1,1,1,1,5,6,6,6,6,10,11}
如果dp[j] == j 说明,集合中的子集总和正好可以凑成总和j,理解这一点很重要。
如何理解这句话:
背包容量为j,装的最大的价值正好为j,背包是由物品填装的,背包中的物品正好把背包给填满了,背包中的物品表示一个子集。
代码
class Solution {
public boolean canPartition(int[] nums) {
//可以当作0 1背包问题来处理
//背包的容量:sum/2
//物品 nums,其中nums中的数值既表示重量又表示价值。
//dp数组的定义
//dp[j]表示容量为j的背包,能装下的最大的价值
//递推公式
//dp[j]=max(dp[j],dp[j-weight[i]]+value[i]) 加物品i和不加物品i
//应用到这道题目
//dp[j]=max(dp[j],dp[j-nums[i]]+nums[i]) nums[i] 既表示物品i的重量也表示物品i的价值
//遍历
int sum=0;
for(int num:nums){
sum+=num;
}
if (sum % 2 == 1) return false;
int target=sum/2;
int [] dp=new int[target+1];
for(int i=0;i<nums.length;i++){//先遍历物品
for(int j=target;j>=nums[i];j--){
dp[j]=Math.max(dp[j],dp[j-nums[i]]+nums[i]);
}
}
System.out.println(Arrays.toString(dp));
if(dp[target]==target) return true;
return false;
}
}