1. 关于动规-01背包
缓慢理解用意中 (领悟+3)
01背包原始思路:背包容量为j时能凑到的最大物品价值dp[j]
01背包分情形:如果背包中的每个物品只能用一次:
- 完全装满背包……
- 1.1 能否完全装满? dp[targetsum]==targetsum
- 1.2 完全装满背包有几种组合方法? dp[j]=dp[j]+dp[j-weight[i]]
- 尽量装满背包…… (100%纯血的01背包)
- 2.1 尽量装满背包的最大价值/重量? dp[targetsum]
- 返回两个背包的最小重量差? sums-2*dp[targetsum]
- 2.2 当背包重量有两个时(j1,j2),尽量装满背包时的最大数量(*此处不是价值也不是重量,而是物品数量)(待优化) dp[j1][j2]=Math.max(dp[j1][j2], 1 + dp[j1-nums[i]'count][j2-nums[i]'count])
- 2.1 尽量装满背包的最大价值/重量? dp[targetsum]
2. 背包思路
1. 将问题转为背包问题:
step1,要素察觉
发现【两个子集】即为要素察觉
=>集中关注一个子集,演化为在背包里取哪些物品来满足达成该子集(=背包容量)的问题
step2,排除情况
排除情况:哪些情况可以提前排除/及时剪枝?
step3,背包概念化
- 背包:背包容量是多少?=>该子集要达到的目的是什么?如sum/2
- 物品:石头们stones。
- 物品重量是什么?
- 物品价值是什么?(有的时候物品重量和价值相同)
- 物品特性是什么?每个元素用一次(=>这是01背包)
- 01背包递推公式:(需要01背包分情形) 如dp[j] v.s dp[j-weight[i]]+val[i]
2. 正式写背包代码:
- dp数组:j代表什么?dp[j]代表什么?
- 初始化:需要考虑初始化吗?当j=0?当i=0?
- 递推公式:如Math.max(dp[j],dp[j-weight[i]]+val[i])
- 遍历顺序:一维滚动数组dp[j],物品增,背包减
3. 前置基础
- 遍历字符串
- char[] charArray = str.toCharArray() 搭配 for(char ch : charArray) …
4. 例题
lc416 分割等和子集 / 背包情况1.1 能否完全装满?
思路
-
将问题转化为背包问题:
- step1,
- 要素察觉两个子集≈只需要考虑其中一个子集≈在nums中取出若干物品令子集(背包)被装满≈01背包装满的可能性
- step2,
- 排除情况若sum/2为小数,则return false
- step3,
- 背包概念化
- 背包:sum/2
- 物品:正整数们nums,物品价值nums[i],物品重量nums[i],物品特性每个物品只用一次(=>得:这是01背包)
- 01背包情况1.1:能否完全装满? dp[targetsum]==targetsum
- 对应递推公式: dp[j] v.s dp[j-weight[i]]+val[i]
- step1,
-
正式写背包代码:
- dp数组:dp[j]。data type <int[]>。当背包容量j为sum/2时,dp[j]表示是否nums中能凑出的最大物品价值(子集和)
- 初始化:考虑两种情况 (1) i=0时的dp[j] (2) j=0时的dp[i]:
(1) 如果i=0,则dp[j] = Math.max(dp[j], dp[j-nums[i]]+nums[i])表现为只有第1个数字时dp[j]的最大价值,也就是nums[0],递推公式可以推到,所以不用特地初始化
(2) 肯定是0,默认了 - 递推公式:Math.max(dp[j],dp[j-nums[i]]+nums[i])
- 遍历顺序:物品i增,背包j减
易错点
- 初始化问题
考虑两种情况 (1) i=0时的dp[j] (2) j=0时的dp[i]:
(1) 如果i=0,则dp[j] = Math.max(dp[j], dp[j-nums[i]]+nums[i])表现为只有第1个数字时dp[j]的最大价值,也就是nums[0],递推公式可以推到,所以不用特地初始化
(2) 肯定是0,默认了 - 剪枝提效
每遍历到第i个数字时都核对一遍,如有就可以提前返回
代码实现
class Solution {
public boolean canPartition(int[] nums) {
int sums= Arrays.stream(nums).sum();
int targetsum = sums/2;
if(sums%2!=0) return false;
//创建dp数组,数组长度:单个子集和从0~targetsum的数量,即targetsum+1
int[] dp=new int[targetsum+1];
//递推
for(int i=0;i<nums.length;i++){
for(int j=targetsum;j>=nums[i];j--){
dp[j] = Math.max(dp[j], dp[j-nums[i]]+nums[i]); //比较加了i这个数和没用i这个数之前的dp[j]哪个大
}
if(dp[targetsum]==targetsum) return true; //剪枝:每遍历到第i个数字时都核对一遍,如有就可以提前返回
}
return dp[targetsum]==targetsum;
}
}
**
lc1049 最后一块石头的重量 II / 背包情况2.1 尽量装满背包的最大价值/重量?(纯血01背包)
思路
-
将问题转化为背包问题:
- step1,
- 要素察觉 发现【两个子集】:分成两组石头,让他们的重量尽量相等,可得到最小重量,设其中一堆石头重量为x,则最小重量=|s-(sum-x)|=sum-2x
=>只要考虑怎么尽量让一堆石头的重量接近sum/2=>在stones中凑一些石头来尽量装满重量为sum/2的背包
=>这是背包
- 要素察觉 发现【两个子集】:分成两组石头,让他们的重量尽量相等,可得到最小重量,设其中一堆石头重量为x,则最小重量=|s-(sum-x)|=sum-2x
- step2,
- 排除情况 n/a
- step3,
- 背包概念化
- 背包:sum/2
- 物品:石头们stones。物品重量stones[i],物品价值stones[i],物品特性每个石头用一次(=>这是01背包)
- 01背包情况2.1:尽量装满容量为?的背包(而不是完全装满) dp[targetsum]
- 对应递推公式: dp[j] v.s dp[j-weight[i]]+val[i]
- step1,
-
正式写背包代码:
- dp数组:dp[j]
- 初始化:全默认
- 递推公式:Math.max(dp[j],dp[j-stones[i]]+stones[i])
- 遍历顺序:物品增,背包减
易错点
n/a
代码实现
class Solution {
public int lastStoneWeightII(int[] stones) {
int sums=Arrays.stream(stones).sum();
int targetsum=sums/2;
//创建dp:从背包重量为0~targetsum
int[] dp=new int[targetsum+1];
//初始化
//j=0:默认 i=0:dp[j]=nums[i],第一层遍历就能直接得出,默认
//递推公式
for(int i=0;i<stones.length;i++){
for(int j=targetsum;j>=stones[i];j--){
dp[j]=Math.max(dp[j],dp[j-stones[i]]+stones[i]);
}
}
return sums-2*dp[targetsum];
}
}
**
lc494 目标和 / 背包情况1.2 完全装满背包有几种组合方法?
思路
-
将问题转化为背包问题:
- step1,
- 串联正数的方法有两种,+/-
- => 要素察觉 可以把数组分为【两个子集】,add数组和minus数组
=> 只关注一个子集如add数组(≥0),设add数组的值为x,则minus数组为sum-x,此外x-(sum-x) = target,则x=(sum+target)/2
=> 在nums中凑一些数让他们的最大价值=(sum-target)/2
- step2,
- 排除情况
- |target|>sum则无解
- (sum+target)/2得小数则无解
- step3,
- 背包概念化
- 背包:(sum-target)/2
- 物品:正整数们nums,物品重量nums[i],物品价值nums[i],物品特性每个正整数用一次=>这是01背包
- 01背包情况2.2:完全装满背包,有几种组合方法
- 对应递推公式:dp[targetsum]=dp[targetsum]+dp[targetsum-weight[i]] ——即,dp[5]=dp[0]+dp[1]+…+dp[4]
- step1,
-
正式写背包代码:
- dp数组:dp[j],在正数子集和为j时,能够凑到和为j的方法数量有dp[j]种
- 初始化:dp[0]…
- 递推公式:dp[j]+=dp[j-nums[i]]
- 遍历顺序:物品增,背包减
易错点
- 注意这里考虑的是正整数数组,如果当前数组目标和为负,就扭为正
- 初始化问题
- 排除情况:什么情况要排除?凑出了个小数,或target绝对值超出数组总和范围
代码实现
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sums= Arrays.stream(nums).sum();
int targetsum=(sums+target)/2;
//易错1: 排除情况?
if(Math.abs(target)>sums) return 0;
if((sums+target)%2!=0) return 0;
//易错2:只考虑正数数组
if(targetsum<0) targetsum=-targetsum;
//创建dp
int[] dp=new int[targetsum+1];
//初始化
//易错3:(1)j=0?一种办法,即正数数组为空; (2)i=0?一种办法,可以由递推公式推导出,默认
dp[0]=1;
//递推
for(int i=0;i<nums.length;i++){
for(int j=targetsum;j>=nums[i];j--){
dp[j]+=dp[j-nums[i]];
}
}
return dp[targetsum];
}
}
**
lc474 一和零 / 背包情况2.2 当背包重量有两个时(j1,j2),尽量装满背包时的最大数量?
思路
-
将问题转化为背包问题:
- step1,
- 串联正数的方法有两种,+/-
- => 要素察觉 分成【两个子集】一个要留下的,一个要撇除的。尽量让1和0数量更少的元素留在子集中,把1和0更多的元素子集撇除
=>只关注留下的子集,即用尽量多的物品数量(而非物品价值或重量)装满背包
- step2,
- 排除情况
- n/a
- 剪枝:遍历字符串str[i]时可以提前排除长度超过m+n的,略剪聊胜于无(待优化)
- step3,
- 背包概念化
- 背包容量:有两个j?m和n——似乎需要三层嵌套遍历
- 物品:字符串们strs,物品价值strs[i],物品重量strs[i]中的1的个数和0的个数,物品特性每个字符串只能用一次
- 01背包情况2.2:背包容量有多个时,如背包容量为[j1][j2],能不能凑到最大物品数量dp[j1][j2]
- 对应递推公式:dp[j1][j2]=Math.max(dp[j1][j2], 1 + dp[j1-nums[i]'count][j2-nums[i]'count])
- step1,
-
正式写背包代码:
- 创建dp数组:dp[j1][j2],在当规定’0’容量为j1、'1’容量为j2时,可以凑到的最长数组长度为dp[j1][j2]
- 初始化:dp[0][0]=0
- 递推公式:dp[j1][j2]=Math.max(dp[j1][j2], dp[j1-countZeroOne(strs(i))][j2-countZeroOne(strs(i))]+1) ——因为最后加上了strs(i)这个字符串,所以dp[j1][j2]相当于长度+1
- 遍历顺序:字符串们i顺序遍历,'0’与’1’的数量j1和j2倒序遍历
易错点
n/a
待优化
效率太低,需要优化
代码实现
class Solution {
public int findMaxForm(String[] strs, int m, int n) {
//排除情况:?
//创建dp
int[][] dp = new int[m+1][n+1];
//初始化
dp[0][0]=0;
//递推公式
//易错:注意不要搞错[]和(),还有countZeroOne return的是数组,需要索引提取数字的
for(int i=0;i<strs.length;i++){
//剪枝1:如果当前字符串长度>m+n,肯定不行
if(strs[i].length()>m+n) continue;
for(int j1=m;j1>=countZeroOne(strs[i])[0];j1--){
for(int j2=n;j2>=countZeroOne(strs[i])[1];j2--){
dp[j1][j2]=Math.max(dp[j1][j2], dp[j1-countZeroOne(strs[i])[0]][j2-countZeroOne(strs[i])[1]]+1);
}
}
}
return dp[m][n];
}
//计算每个字符串中的0和1的个数
public int[] countZeroOne(String str){
int[] counts= new int[2];
char[] chars = str.toCharArray();
for(char ch: chars){
if(ch =='0') counts[0]++;
if(ch =='1') counts[1]++;
}
return counts;
}
}