背包问题
有n件物品和一个最多能背重量为w 的背包。第i件物品的重量是weight[i],得到的价值是value[i] 。每件物品只能用一次,求解将哪些物品装入背包里物品价值总和最大。
二维数组
- 确定dp数组以及下标的定义
- dp[i][j] 表示从下标为[0-i]的物品里任意取,放进容量为j的背包,价值总和最大是多少。
- 确定递推公式
- 放物品i:dp[i-1][j-weight[i]] + value[i] 即放入第i个物品;剩下的只能从下标[0,i-1]的物品里选取,背包剩下的总负载量减少weight[i],放入物品i后增加的价值为value[i[
- 不放物品i:dp[i-1][j] 即背包容量为j但是不放第i个物品
- 所以dp[i][j] = Math.max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i])
- dp数组如何初始化
- dp[i][0]都是0,因为负载量是0无法放任何物品
- 因为递推公式中i是从i-1来的,所以要初始化i=0的情况;dp[0][j]有两种情况
- j < weight[0]那么dp[0][j] = 0;因为无法放入第0个物品
- j>=weight[0]那么dp[0][j] = value[0];因为放入第0个物品,得到第0个物品的价值
- 确定遍历顺序
- 先遍历物品再遍历物品重量
- 注意如果 遍历到当前背包的总负载量小于物品重量那么肯定是不能放这个物品的
- 即 j < weight[i]
- 那么前i-1个物品放入背包的最大价值就是当前情况的最大价值
- 即dp[i][j] = dp[i-1][j]
- 完整代码
public static void testWeightBagProblem(int[] weight, int[] value, int bagSize){
// 创建dp数组
int goods = weight.length; // 获取物品的数量
int[][] dp = new int[goods][bagSize + 1];
// 初始化dp数组
// 创建数组后,其中默认的值就是0, 所以不用考虑dp[i][0]
// 我们从j=weight[0]开始遍历,所以就不用考虑j <weight[0]的情况
for (int j = weight[0]; j <= bagSize; 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]) {
/**
* 当前背包的容量都没有当前物品i大的时候,是不放物品i的
* 那么前i-1个物品能放下的最大价值就是当前情况的最大价值
*/
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]);
}
}
}
一维数组(滚动数组)
- 确定dp数组的定义
- dp[j]表示容量为j的背包,所负载的物品最大价值为dp[j]
- 递推公式
- 放入物品i:dp[j-weight[i]] + value[i] {容量为j-weight[i]的背包的最大价值加上物品i的价值}
- 不放入物品i:dp[j] {跳到下一个物品,相当于二维数组的dp[i-1][j])
- dp[j] = Math.max(dp[j] , dp[j-weight[i]] + value[i])
- 一维数组如何初始化
- dp[0]= 0 因为容量为0的背包最大价值就是0
- 对于其他位置,假设物品价值都是大于0的,就默认初始为0即可
- 遍历顺序
- 先遍历物品再遍历背包
- 背包遍历时,遍历顺序与二维数组不同,现在这种情况背包从大到小
- 因为如果正序遍历会导致重复使用同一个物品
- 而倒序遍历每次取得的状态不会与上一个状态重合
- 为什么二维数组不用倒序呢
- 因为dp[i][j]是从dp[i-1][j]计算而来,本层的dp[i][j]不会被覆盖
- 完整代码
public static void testWeightBagProblem(int[] weight, int[] value, int bagWeight){
int wLen = weight.length;
//定义dp数组:dp[j]表示背包容量为j时,能获得的最大价值
int[] dp = new int[bagWeight + 1];
//遍历顺序:先遍历物品,再遍历背包容量
// 且背包容量为倒序遍历
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]);
}
}
}
416. 分割等和子集
给你一个 只包含正整数 的 非空 数组
nums
。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
class Solution {
public boolean canPartition(int[] nums) {
if(nums==null || nums.length==0){
return false;
}
int n =nums.length;
int sum = 0;
for(int num:nums){
sum+=num;
}
// 总和为奇数不能平分直接返回
if(sum%2!=0)
return false;
int target = sum/2;
// dp[j] 表示背包所能装的总容量是j
// 所以当 dp[target] == target表示背包装满了
// 表示可以分成两个不同的子集
int [] dp = new int[target+1];
for(int i=0;i<n;i++){
for(int j =target;j>=nums[i];j--){
// 物品i的重量是nums[i]其价值也是nums[i]
// 要么不放物品 dp[j]
// 要么放物品 dp[j-nums[i]] + nums[i] {即放完i剩下的重量+放上i的价值}
dp[j] = Math.max(dp[j],dp[j-nums[i]]+nums[i]);
}
}
return dp[target] == target;
}
}
01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);
题目中物品是nums[i],重量是nums[i],价值也是nums[i],背包体积是sum/2。
dp[j]的数值一定是小于等于j的
因为本题给的是正整数所以非0下标初始值为0即可,否则要把非零下标设置为负无穷
背包要放入的商品(集合里的元素)重量为 元素的数值,价值也为元素的数值
背包如果正好装满,说明找到了总和为 sum / 2 的子集。
背包中每一个元素是不可重复放入。