前言
1、分割等和子集
2、最后一块石头的重量 II
3、目标和
一、分割等和子集(力扣416)
给你一个 只包含正整数 的 非空 数组 nums 。请你判断是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
思路:
因为元素我们只能用一次,因此我们要使用的是01背包。
动规五部曲
1、确定dp数组以及下标的含义
dp[j]:背包总容量是j,放进物品后,背的最大重量为dp[j]。
本题中每一个元素的数值既是重量,也是价值。
2、确定递推公式
dp[j] = Math.max(dp[j] , dp[j - nums[i] ] + nums[i]);
3、初始化
dp[0]一定是0
同01背包,一维dp的初始化
4、遍历顺序
先遍历nums[]数组中的数,然后倒序遍历背包容量
5、打印dp[]数组
输入[1,5,11,5] 为例
dp[11] == 11,说明可以将这个数组分割成两个子集,使得两个子集的元素和相等。
class Solution {
public boolean canPartition(int[] nums) {
int sum=0;
for(int i=0;i<nums.length;i++){
sum += nums[i];
}
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[target]==target) return true;
else return false;
}
}
二、最后一块石头的重量 II(力扣1049)
有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。
思路: 尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了。
因为元素我们只能用一次,因此我们要使用的是01背包。
动规五部曲
1、确定dp数组以及下标的含义
dp[j]:容量为j的背包,最多可以背最大重量为dp[j]。
本题中每一个元素的数值既是重量,也是价值。
2、确定递推公式
dp[j] = Math.max(dp[j] , dp[j - stones[i] ] + stones[i]);
3、初始化
dp[0]一定是0
同01背包,一维dp的初始化
4、遍历顺序
先遍历nums[]数组中的数,然后倒序遍历背包容量
5、打印dp[]数组
输入:[2,4,1,1],此时target = (2 + 4 + 1 + 1)/2 = 4 ,dp数组状态图如下:
最后dp[target]里是容量为target的背包所能背的最大重量。
那么分成两堆石头,一堆石头的总重量是dp[target],另一堆就是sum - dp[target]。
class Solution {
public int lastStoneWeightII(int[] stones) {
int sum=0;
for(int i=0;i<stones.length;i++){
sum += stones[i];
}
int target = sum/2;
int[] dp = new int[target+1];
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]);
}
}
//那么分成两堆石头,一堆石头的总重量是dp[target],另一堆就是sum - dp[target]
return sum - 2*dp[target];
}
}
三、目标和(力扣494)
给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
思路:
先转化为动态规划问题:
一波神奇的推导:
本题要使表达式结果为target,
left组合为“+”组合
right组合为“-”组合
既然为target,那么就一定有 left组合 - right组合 = target。
left + right = sum,sum是固定的。
right = sum - left
**left - (sum - left) = target 推导出==> left = (target + sum)/2 **
如果出现不能整除的情况也就是说明没有这个组合
target是固定的,sum是固定的,left就可以求出来。
此时问题就是在集合nums中找出和为left的组合。
这次和之前遇到的背包问题不一样了,之前都是求容量为j的背包,最多能装多少。本题则是装满有几种方法。其实这就是一个组合问题了。
动规五部曲
1、确定dp数组以及下标的含义
dp[j]:填满j这么大容积的包,有dp[j]种方法
2、确定递推公式
只要搞到nums[i],凑成dp[j]就有dp[j - nums[i]] 种方法。
例如:dp[j],j 为5,
- 已经有一个1(nums[i]) 的话,有 dp[4]种方法 凑成 容量为5的背包。
- 已经有一个2(nums[i]) 的话,有 dp[3]种方法 凑成 容量为5的背包。
- 已经有一个3(nums[i]) 的话,有 dp[2]中方法 凑成 容量为5的背包
- 已经有一个4(nums[i]) 的话,有 dp[1]中方法 凑成 容量为5的背包
- 已经有一个5 (nums[i])的话,有 dp[0]中方法 凑成 容量为5的背包
那么凑整dp[5]有多少方法呢,也就是把 所有的 dp[j - nums[i]] 累加起来。
dp[j] += dp[j-nums[i]]
3、初始化
dp[0]一定是1,因为dp[0]是在公式中一切递推结果的起源,如果dp[0]是0的话,递推结果将都是0。
4、遍历顺序
先遍历nums[]数组中的数,然后倒序遍历背包容量
5、打印dp[]数组
输入:nums: [1, 1, 1, 1, 1], S: 3
bagSize = (S + sum) / 2 = (3 + 5) / 2 = 4
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sum = 0 ;
for(int i=0;i<nums.length;i++){
sum+=nums[i];
}
if((target+sum)%2!=0 )return 0;
//如果target过大 sum将无法满足
if ( target < 0 && sum < -target) return 0;
int bagSize = (target+sum)/2;
int[] dp = new int[Math.abs(bagSize)+1];
dp[0]=1;
for(int i=0;i<nums.length;i++){
for(int j=bagSize;j>=nums[i];j--){
dp[j] += dp[j-nums[i]];
}
}
return dp[bagSize];
}
}
总结
即一个商品如果可以重复多次放入是完全背包,而只能放入一次是01背包,写法还是不一样的。