01.背包问题 二维
代码随想录
背包区别:
01背包: n种物品,每种物品只有1个
完全背包:n种物品,每种物品有无限个
多重背包:n种物品,每种物品数量各不相同
暴力解法:每个物体只有取与不取两种状态,枚举每一种取的可能,时间复杂度为(2^n)
动态规划:
- dp[i][j]含义:[0,i]物品任取放到容量为j的背包里 。
- 关于dp数组的定义问题,up是先给dp数组,再推递推关系。实际上应该先搞清楚问题与子问题之间的递推关系,在定义dp数组。
- 首先对于整个问题:m个物品,背包容量最大为n。
- 初步将问题分解为:在已经知道了前m-1个物品的所有最优解的情况下(即无论背包容量多少),再加上第m个物品的情况。
- 此时有三种情况:
- 1、第m个的重量超过了背包容量n,不可能放下第m个,所以此时的最优解,就是m-1个物品,背包容量为n的最优解。
- 2、第m个的重量小于n,可以放但是不放,此时的最优解仍然是m-1个物品,背包容量为n的最优解。
- 3、第m个的重量小于n,可以放而且真的放了,此时最优解就是第m个物品的价值,加上,m-1个物品时背包容量为(n-第m个物品的重量)的最优解。
- 根据递推关系发现,构造问题的最优解,不仅需要一个i来标识选择物品的范围,还需要一个j,来求出在0到i选择物品且背包容量为j时的最优解。从而将dp数组确定为dpij
当i放进去时,那么这时候整个物品集就被分成两部分,1到i-1和第i个,而这是i是确定要放进去的,那么就把j空间里的wi给占据了,只剩下j-wi的空间给前面i-1,那么只要这时候前面i-1在j-wi空间里构造出最大价值,即dp【i-1】【j-wi】,再加上此时放入的i的价值vi,就是dpij了
- 递推公式
- 不放物品 i :dp[j][j] = dp [i-1][j];
- 放物品 i :dp[i][j] = dp[i-1][[j-weight[i]](上一个不放物品i的价值)+ value[i]
- 以上两者取最大值
- 初始化
- dp[i][j]是由dp[i-1][j],也就是从上而下推出来的
- dp[i][j] 是由dp[i-1][[j-weight[i]]大致推出来的,也就是左上角方向
- 最后确定初始化最上侧和最左侧元素
- 最左侧:背包容量为0时,最大价值应该是0,所以最左侧全部初始化为0
- 最上侧:除背包容量小于物品0重量之外,其余都初始化为物品0元素的价值
- 非零下标:递推公式并不会与本身比较,初始化成任何书都无所谓
- 遍历顺序
- 先遍历物品再遍历背包
- 先遍历背包再遍历物品
- 以上两种方式在二维面板数组都能确保递归公式的左上方和上侧都先有值
代码
class solution{
public int solveBagProblem(int[] weight,int[] values,int bagSize){
int goods = weight.length;
int[][] dp = new int[goods][bagSize+1];
for(int j = weight[0];j<=bagSize;j++){
dp[0][j]=values[0];
}
for(int i = 1;i<goods;i++){
for(int j = 1;j<=bagSize;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]])+values[i]);
}
}
}
return dp[goods-1][bagSize];
}
}
01.背包问题 一维
代码随想录
- dp[j] : 容量为 j 的背包所背的最大价值
- 递推公式:消除掉二维时的 i 向量
- dp[ j ] = Math.max( dp[j] ,dp[j-weight[i]) + value [ i ] )
- 初始化
- 下标为0 :初始为0很好理解
- 下标非 0 :递推公式中dp数组在推导的时候一定是取价值最大数,如果题目给出的价值都是正整数那么非0下标都初始化为0就可以,最终会让dp数组在递推公式中取的最大的价值,而不是被初始值覆盖
- 总结:均初始化为0
- 遍历顺序:
- 正序遍历物品
- 反向遍历背包容量
- 解释:
- 为什么要倒序遍历背包?:保证每个物品只被添加一次
- 正序遍历物品
终于搞懂为啥要倒叙遍历了。
首先要明白二维数组的递推过程,然后才能看懂二维变一维的过程。
假设目前有背包容量为10,可以装的最大价值, 记为g(10)。
即将进来的物品重量为6。价值为9。
那么此时可以选择装该物品或者不装该物品。如果不装该物品,显然背包容量无变化,这里对应二维数组,其实就是取该格子上方的格子复制下来,就是所说的滚动下来,直接g【10】 = g【10】,这两个g【10】要搞清楚,右边的g【10】是上一轮记录的,也就是对应二维数组里上一层的值,而左边是新的g【10】,也就是对应二维数组里下一层的值。
如果装该物品,则背包容量= g(10-6) = g(4) + 9 ,也就是 g(10) = g(4) + 6 ,这里的6显然就是新进来的物品的价值,g(10)就是新记录的,对应二维数组里下一层的值,而这里的g(4)是对应二维数组里上一层的值,通俗的来讲:你要找到上一层也就是上一状态下 背包容量为4时的能装的最大价值,用它来更新下一层的这一状态,也就是加入了价值为9的物品的新状态。
这时候如果是正序遍历会怎么样? g(10) = g(4) + 6 ,这个式子里的g(4)就不再是上一层的了,因为你是正序啊,g(4) 比g(10)提前更新,那么此时程序已经没法读取到上一层的g(4)了,新更新的下一层的g(4)覆盖掉了,这里也就是为啥有题解说一件物品被拿了两次的原因。
——B站录友
代码
public int solveBagProblemTwo(int[] weight,int[] values,int bagSize){
int goods = weight.length;
int[] dp = new int[bagSize+1];
for (int i = 0; i < goods; i++) {
for (int j = bagSize; j >= weight[i]; j--) {
dp[j] = Math.max(dp[j],dp[j-weight[i]]+values[i]);
}
}
return dp[bagSize];
}
416.分割等和子集
代码随想录
只有确定了如下四点,才能把01背包问题套到本题上来。
- 背包的体积为sum / 2
- 背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值
- 背包如果正好装满,说明找到了总和为 sum / 2 的子集。
- 背包中每一个元素是不可重复放入。
class Solution {
public boolean canPartition(int[] nums) {
int sum = Arrays.stream(nums).sum();
if (sum % 2 != 0) {
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]);
if(dp[j]==target)
return true;
}
}
return dp[target]==target;
}
}