round1/day16&17/动规4 - 01背包总结

1. 关于动规-01背包

缓慢理解用意中 (领悟+3)
01背包原始思路:背包容量为j时能凑到的最大物品价值dp[j]
01背包分情形:如果背包中的每个物品只能用一次:

  1. 完全装满背包……
    • 1.1 能否完全装满? dp[targetsum]==targetsum
    • 1.2 完全装满背包有几种组合方法? dp[j]=dp[j]+dp[j-weight[i]]
  2. 尽量装满背包…… (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. 将问题转为背包问题:

step1,要素察觉

发现【两个子集】即为要素察觉
=>集中关注一个子集,演化为在背包里取哪些物品来满足达成该子集(=背包容量)的问题

step2,排除情况

排除情况:哪些情况可以提前排除/及时剪枝?

step3,背包概念化

  1. 背包:背包容量是多少?=>该子集要达到的目的是什么?如sum/2
  2. 物品:石头们stones。
    • 物品重量是什么?
    • 物品价值是什么?(有的时候物品重量和价值相同)
    • 物品特性是什么?每个元素用一次(=>这是01背包)
  3. 01背包递推公式:(需要01背包分情形) 如dp[j] v.s dp[j-weight[i]]+val[i]

2. 正式写背包代码:

  1. dp数组:j代表什么?dp[j]代表什么?
  2. 初始化:需要考虑初始化吗?当j=0?当i=0
  3. 递推公式:如Math.max(dp[j],dp[j-weight[i]]+val[i])
  4. 遍历顺序:一维滚动数组dp[j],物品增,背包减

3. 前置基础

  1. 遍历字符串
    • char[] charArray = str.toCharArray() 搭配 for(char ch : charArray) …

4. 例题

lc416 分割等和子集 / 背包情况1.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]
  2. 正式写背包代码:

    • 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. 初始化问题
    考虑两种情况 (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,默认了
  2. 剪枝提效
    每遍历到第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背包)

思路

  1. 将问题转化为背包问题:

    • step1,
      • 要素察觉 发现【两个子集】:分成两组石头,让他们的重量尽量相等,可得到最小重量,设其中一堆石头重量为x,则最小重量=|s-(sum-x)|=sum-2x
        =>只要考虑怎么尽量让一堆石头的重量接近sum/2=>在stones中凑一些石头来尽量装满重量为sum/2的背包
        =>这是背包
    • 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]
  2. 正式写背包代码:

    • 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 完全装满背包有几种组合方法?

思路

  1. 将问题转化为背包问题:

    • 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]
  2. 正式写背包代码:

    • dp数组:dp[j],在正数子集和为j时,能够凑到和为j的方法数量有dp[j]种
    • 初始化:dp[0]…
    • 递推公式:dp[j]+=dp[j-nums[i]]
    • 遍历顺序:物品增,背包减

易错点

  1. 注意这里考虑的是正整数数组,如果当前数组目标和为负,就扭为正
  2. 初始化问题
  3. 排除情况:什么情况要排除?凑出了个小数,或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),尽量装满背包时的最大数量?

思路

  1. 将问题转化为背包问题:

    • 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])
  2. 正式写背包代码:

    • 创建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;
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值