代码随想录训练营 Day42
今日任务
01背包问题二维
01背包问题一维
416.分割等和子集
语言:Java
背包问题辨析
01背包:物品只有一个
完全背包:物品有无数个
多重背包:不同的物品数量不同
分组背包:按组打包,每组最多选一个
01背包问题-二维数组
背包问题:有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
- dp数组及下标含义:从0-i个物品中挑选物品,放到最大容量为j的背包中得到的最大价值为dp[i][j]
- 递归公式:
if(j < weight[i]) dp[i][j] = dp[i-1][j]
dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i])
dp[i-1][j]
:代表不将第i个物品放入容量为j的背包中的价值
dp[i-1][j-weight[i]]
:代表从0~i-1个物品中挑选物品,放到最大容量为j-weight[i]的背包中得到的最大价值
dp[i-1][j-weight[i]]+value[i]
:代表将第i个物品放入容量为j的背包后的价值 - 初始化:
①dp[i][0]=0
,不管是遍历第几个物品,如果背包最大容量就是0,那么得到的最大价值也一定是0;
②dp[0][j]
:因为目前只有物品0,所以只需要考虑物品0的重量和价值
if(weight[0] > bagweight[j]) dp[0][j] = 0;
if(weight[0] <= bagweight[j] dp[0][j] = value[0];
- 确定遍历顺序:先遍历物品或先遍历背包都是可以的,在初始化的时候要紧盯递归公式,明确递归公式中需要的值来自于哪里;
① 先遍历物品i,当我们遍历到dp[i][j]时,dp数组中第i行以上的所有元素都已经被赋值了,所以我们可以正确得到dp[i][j]的值
② 先遍历背包j,当我们遍历到dp[i][j]时,dp数组中第j列左侧所有的列都已经被赋值,虽然我们需要用到第i-1行第j列的元素,但因为我们在遍历物品的时候是从小到大遍历的,所以dp[i-1][j]也已经被赋值了,我们可以正确得到dp[i][j]的值 - 打印dp数组
01背包问题-一维滚动数组
二维数组的改进,我们注意到二维数组的递归公式中,我们需要使用到的都是上一行的数值,所以可以考虑用一维数组
- dp数组及下标含义:dp[j]代表容量为j的背包,可以获得的物品最大价值为dp[j]
- 递归公式:
if(j < weight[i]) dp[j] = dp[j]
dp[j] = max(dp[j], dp[j - weight[i]] + value[i]
- 初始化:dp[0]=0,根据递归公式,我们每次取的是最大值,所以如果题目给的值都是正数,那么我们直接将其他下标处的值也初始化为0即可
- 遍历顺序:
① 为什么要倒序遍历?
保证物品i只被放入一次
以物品0为例,weight[0]=1,value[0]=15,正序遍历:
dp[1] = max(dp[1], dp[1 - weight[0]] + value[0]) //15
dp[2] = max(dp[2], dp[2 - weight[0]] + value[0]) //30
我们可以看到,如果目前只是在放物品0的状态,那么在背包容量大于等于1时,所有的背包最大价值都应该为15(因为是01背包,一种物品只有一个),而如果按照背包容量从小到大遍历,就会有重复计入相同物品的问题。
② 为什么倒序遍历可以保证只计入物品一次呢?
依旧以物品0为例,weight[0]=1,value[0]=15,倒序遍历:
dp[2] = max(dp[2], dp[2 - weight[0]] + value[0]) //15
dp[1] = max(dp[1], dp[1 - weight[0]] + value[0]) //15
③ 为什么二维数组不用倒序遍历呢?
可以参考下图中的过程理解:
当我们使用二维数组计算3的值时,需要参考上一行1和2的值,但又因为我们遍历完第i-1行后就不会再修改该行中元素的值,所以不用担心下一行遍历顺序的问题;
当我们使用一维数组计算3的值时,需要参考当前数组1和2位置的旧值,如果使用前序遍历而非倒序遍历,那么在遍历到3的位置时,1位置的值已经不再是旧值,而是新值,会导致3的结果出错。
④ 为什么必须先遍历物品再遍历背包,可以反过来吗?
必须先遍历物品再遍历背包,反过来相当于背包只放入了一个物品
先遍历背包再遍历物品的循环逻辑如下:
我们依旧是要搞清楚dp数组的含义以及i和j的含义,dp现在只是一个一维数组,如果先遍历背包,相当于先固定j,再通过不断改变i来修改dp[i][j]的值。因为元素初始化为0以及后序遍历的缘故,相当于每次只向背包中放入了一个物品,这和dp数组的含义产生了冲突。for(int j = bagweight.length; j >= weight[i]; j--){ for(int i = 0; i < weight.length; i++){ //... } }
- 打印dp数组
416. 分割等和子集
链接:https://leetcode.cn/problems/partition-equal-subset-sum/
题目转换:
① 背包:让背包容量代表子集的和
② 物品:让数组中的元素代表物品
class Solution {
public boolean canPartition(int[] nums) {
//1.明确dp数组及下标含义: 容量为j的背包得到的子集和最大为dp[j]
//2.递归公式: dp[j]=max(dp[j],dp[j-weight[i]]+value[i])
//3.初始化: 0
//4.遍历顺序: 外层遍历物品,内层遍历背包,背包从后向前遍历
//5.打印dp数组
int sum = 0;
for(int i = 0; i < nums.length; i++){
sum += nums[i];
}
if(sum %2 == 1) return false; //和为奇数,肯定不会构成两个相同的背包
int target = sum / 2;
//nums数组中的每个元素代表重量为nums[i]价值为nums[i]的物品
//weight[i]=value[i]=nums[i]
//背包的最大容量就是每个子集能达到的最大的和,根据题意最大为200×100÷2
int[] dp = new int[10001];
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]);
}
}
return dp[target] == target;
}
}