01背包模板
01背包问题:有N件物品,和一个最多能背W重量的背包,第i件物品的重量是weight[i],得到的价值是value[i],每件物品只能使用一次,求将那些物品装入背包价值最大
二维数组写法
思路
- 确定dp数组及其下标含义:dp[i][j]表示,从下标为0-i的物品里任意取,放进容量为j的背包,最大的价值总和
- 推导dp方程:
每件物品有两种选择:
装进背包:dp[i-1][j-weight[i]]+value[i]
,背包减去该物品的重量再加上该物品的价值
不装进背包:dp[i-1][j]
不装,背包价值没变,即背包的价值等于上一个状态的(没遍历第i个物品的状态)价值
所以递推公式:dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i])
- dp数组的初始化:一定要和dp数组的定义吻合
dp[i][0]肯定是0,因为背包不能装东西。
因为i是由i-1推导出来的,所以要先初始化i=0的情况,这时要倒序遍历j,即 j=背包容量 开始递减遍历,倒序是因为要避免多次加入i=0,即物品0,正序会多次加入物品0,这违反了01背包的规则
其余位置初始化最小即可,由于题目只有正整数,所以其余位置初始化为0即可,若题目中有价值为负数的物品,那么dp数组其他位置应该初始化为负无穷。 - 确定遍历顺序:先遍历物品还是先遍历背包容量呢? 其实都可以,因为dp[i][j]的值是由dp[i-1][j-weight[i]]+value[i]和dp[i-1][j]决定的,而这两个值都在dp[i][j]的左上方,所以先遍历物品或者先遍历背包结果是一样的。
代码
public class Demo {
public static void main(String[] args) {
bag_01();
}
public static void bag_01(){
int[] weight = new int[]{1,3,4};
int[] value = new int[]{15,20,30};
int bagWeight = 4;
int[][] dp = new int[weight.length+1][bagWeight+1];
//初始化dp数组
for(int j=bagWeight;j>=weight[0];j--){
dp[0][j] = dp[0][j-weight[0]]+value[0];
}
//先遍历物品,在遍历背包大小
for(int i=1;i<weight.length;i++){//从1开始是因为第一个物品已经在初始化时放进去了,不然i-1会越界
for(int j=0;j<=bagWeight;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]]+value[i]);
}
}
System.out.println(dp[weight.length-1][bagWeight]);//dp[物品个数-1][物品容量位置]
}
}
一维滚动数组写法
思路
把dp[i-1]这一层的结果拷贝到dp[i],上层的结果可以重复利用,即可使用滚动数组
- 确定dp数组及其下标的含义:容量为j的背包,所背物品的最大价值为dp[j]
- 推导dp方程:dp[j] 可以通过dp[j-weigth[i]推导出来
所以递推公式为:dp[j] = max(dp[j],dp[j-weight[i]]+value(i)
- 初始化:dp[0] = 0,一维dp只能是背包容量从大到小,并且先遍历物品再遍历背包容量
因为二维dp遍历的时候dp[-1][j]不会被覆盖,所以可以不用倒序
代码
public class Demo {
public static void main(String[] args) {
bag_01_2();
}
public static void bag_01_2(){
int[] weight = new int[]{1,3,4};
int[] value = new int[]{15,20,30};
int bagWeight = 4;
int[] dp = new int[bagWeight+1];
//先遍历物品,在遍历背包大小
for(int i=0;i<weight.length;i++){
for(int j=bagWeight;j>=weight[i];j--){
dp[j] = Math.max(dp[j], dp[j - weight[i]] + value[i]);
}
}
System.out.println(dp[bagWeight]);
}
}
分割等和子集
416. 分割等和子集
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意:
每个数组中的元素不会超过 100
数组的大小不会超过 200
示例 1:
输入: [1, 5, 11, 5]
输出: true
解释: 数组可以分割成 [1, 5, 5] 和 [11].
示例 2:
输入: [1, 2, 3, 5]
输出: false
解释: 数组不能分割成两个元素和相等的子集.
思路
确定使用什么背包:本题每个数字只能使用一次,所以是01背包
看是否能套用01背包模板,有几个点需要注意
- 物品使用次数:每个数字只能使用一次
- 背包的体积:本题为sum/2
- 物品的价值和重量:本题的物品的价值和重量都是相同的
- 背包装满的条件:重量达到sum/2
套用五部曲
- 确定dp数组及其下标的含义:i代表背包的容量,dp[i]代表重量为i的背包能装下的最大重量
- 推导dp方程:
01背包的递推公式:dp[j] = max(dp[j],dp[j-weight[i]]+value[i])
本题中物品的重量和价值是一样的所以
本题的递推公式:dp[j] = max(dp[j],dp[j-nums[i]]+nums[i])
- dp数组初始化:dp[0] = 0
因为题目给的所有值都是正整数,所以dp数组除开0之外的位置初始化为0即可,如果值有负数,则要初始化为负无穷
因为要保证取到的最小值一定是在递归过程中得到的,而不是被初始值覆盖的。 - 确定遍历顺序:使用一维的dp数组,应该是外层遍历物品,内层遍历背包容量,并且背包容量是从大到小遍历
- 举例推导dp数组:
dp[i]的数组一定是小于i的,因为dp[0] = 0,所以当dp[i] == i时,集合中的子集总和正好可以凑成总和i。
代码
public boolean canPartition(int[] nums) {
int sum = 0;
int[] dp = new int[20001];
for(int value:nums){
sum += value;
}
if(sum%2 == 1) return false;
int target = sum/2;
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[target] == target) return true;
else return false;
}
总结
此题是一道01背包的应用题,主要要正确的理解本题中物品的价值和重量是什么,背包装满的条件是什么。
最后一块石头的重量 II
最后一块石头的重量 II
有一堆石头,每块石头的重量都是正整数。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块石头。返回此石头最小的可能重量。如果没有石头剩下,就返回 0。
示例:
输入:[2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。
提示:
1 <= stones.length <= 30
1 <= stones[i] <= 1000
思路
本题主要还是看到每个石头只能拿一次,然后要求最优解,考虑01背包
想要是石头碰撞从而剩下最少的石头,最直接的方法就是要将所有的石头均分成两份,这样碰撞之后剩下的石头一定是最少的。
所以我们暂时的目标就是求出背包重量为总体石头重量的一半时一直装石头能装下的最大重量
即求dp[target]; target = sum/2;
本题和上一题类似,物品的重量和价值都是一样的
分五步走:
- 确定dp数组及其下标含义:即dp[i]为承重为i的背包所能装下的最大的石头重量
- 推导dp方程:本题物品重量和价值一样,所以dp方程类似01背包
dp[j] = max(dp[j],dp[j-nums[i]]+nums[i])
- 初始化dp数组:价值都是正整数,所以都初始化为0,因为题目数值范围,所以dp最大开30001,但是我们只需要求出总重量的一半,所以dp数组大小为sum/2+1即可
- 确定遍历顺序:使用一维dp数组,物品循环放在外层,背包重量放在内层并且倒序遍历(因为背包容量至少要有第i个物品那么大才能放进去)。
- 举例推导dp数组:
本题dp[target]即为容量为target的背包所能装下的最重的石头重量
那么分成两堆石头,一堆为dp[target],那么另一堆石头就是sum-dp[target]
由于计算target时,sum/2是向下取整,所以sum-dp[target]一定要>=dp[target]
所以最后剩下的石头;(sum-dp[target])-dp[target]
代码
public int lastStoneWeightII(int[] stones) {
int sum = 0;
for(int value:stones){
sum+=value;
}
int[] dp = new int[sum/2+1];
int target = sum/2;
for(int i=0;i<stones.length;i++){
for(int j=target;j>=stones[i];j--){
dp[j] = Math.max(dp[j],dp[j-stones[i]]+stones[i]);
}
}
return sum-dp[target]-dp[target];
}
总结
这题主要是要想到:既然要剩下石头最少,那么就要碰撞更多的石头,也就是要将石头尽可能地均分成两份,这样碰撞留下的石头数量才会最少,所以要求的也就是容量为总重量一半的背包能装下的最多的石头重量,也就转换成了01背包问题,随后带入公式求解dp[target]。
然后注意细节问题:sum/2是向下取整,所以sum-dp[target]肯定是要大于dp[target]的,所以这两部分石头碰撞剩下的石头数量一定是前者减去后者