【力扣一刷】代码随想录day43(动态规划part5 - 背包问题专题: 1049. 最后一块石头的重量II、494. 目标和、474.一和零)

【1049. 最后一块石头的重量 II】中等题

思路:要学会将问题转化为01背包问题求解(我还是不会转换,看了卡哥的思路才会)

题意要求的是【粉碎后最小的可能重量】,想象一下,如果尽可能地将这堆石头分成重量最接近的两堆石头,即达到两边重量最可能平衡的状态,那么这个重量的差值就是题意要求解的最小可能重量。需要两堆石头重量最接近,相当于求容量为sum/2的书包能产生的最大价值,此时对应两堆石头最可能平衡时的重量差。


例子1:输入:stones = [2,7,4,1,8,1]   输出:1

sum = 23,target = 23 / 2 = 11,不可能将这堆石头恰好分成重量相同的两堆,但是最理想的情况下,是容量为11的背包恰好能装满,那么背包里的石头的重量就是11,剩下的没被装入书包的石头的重量就是23-11=12,那么两堆石头的重量差值就是12-11=1,即和为奇数的数组的最小差值至少为1。


例子2:输入:stones = [1,2,2,1]   输出:0

sum = 6,target = 6 / 2 = 3,有可能将这堆石头分为重量相等的两堆,最理想的情况下,是容量为3的书包恰好能装满,那么背包里的石头的重量就是3,剩下没装入书包的石头重量也是3,则重量的差值就是0,即和为偶数的数组的最小差值至少为0。


例子3:输入:stones = [31,26,33,21,40]  输出:5

sum=151,target = 151 / 2 = 75,容量为75的书包最多能装重量为73的石头,那么剩下的石头的重量为151 - 73 = 78,那么最小差值就是 78 - 72 = 5。


步骤:

1、计算总和
2、计算目标容量
3、初始化数组
4、进行递推:

在容量足够的情况下,能考虑放/不放
剪枝(可不写):如果容量为target书包早就满了,直接返回差值即可
5、返回最小差值

class Solution {
    public int lastStoneWeightII(int[] stones) {
        // 1、计算总和
        int sum = 0;
        for(int stone : stones) sum += stone;
        // 2、计算目标容量
        int target = sum / 2;
        // 3、初始化数组
        int[] dp = new int[target+1];
        // 4、进行递推
        for (int i = 0; i < stones.length; i++){
            for (int j = target; j >= stones[i]; j--){ // 容量必须倒序遍历,j >= stones[i]确保足够容量放下i
                dp[j] = Math.max(dp[j], dp[j-stones[i]] + stones[i]); // 在容量足够的情况下,能考虑放/不放
            }
            // 剪枝(可不写):如果容量为target书包早就满了,直接返回差值即可
            if (dp[target] == target) return sum % 2 == 0? 0 : 1; //不能直接返回0,如果sum是奇数,差值为1
        }
        // dp[target]是最平衡的情况下其中一边的重量,另一边的重量是sum - dp[target],返回最小差值
        return (sum - dp[target]) - dp[target];
    }
}
  • 时间复杂度:O(M×N)
  • 空间复杂度:O(N)

【494. 目标和】中等题(用数学推导,得多刷几次)

思路:将问题转化为01背包问题求解

根据题意,要为数组中的每个元素加上符号,求使表达式的值为target的方案数。

如果将数组的元素按符号进行分类,即将一部分看作正数分到数组pos中,剩下的看作负数分到数组neg中,出现 posSum - negSum = target 时这个方案成立。

理想情况下,

posSum + negSum = sum  (1)

posSum - negSum = target  (2)

将(1)+(2)得,posSum = (sum + target) / 2

即如果数组中存在多个元素的和(posSum)等于(sum+target) / 2的情况,则统计共有多少种这样的情况,即得到题目要求的方案数。那么,现在的问题就转换为,求恰好能将容量为(sum+target) / 2的书包装满的方案数 / 元素组合数。

易错点:不能直接将dp[j]定义为容量为 j 的书包产生的最大价值,因为像下面代码那样,只在恰好将容量为posNum装满时次数加1,会忽略前面未装满情况下的组合情况,导致次数少于正确的结果次数。因此,dp[j]应该定义为【恰好装满】容量为 j 的书包的【方案数】。

for (int i = 0; i < nums.length; i++){
    for (int j = posSum; j >= nums[i]; j--){
        int pre = dp[j];
        dp[j] = Math.max(dp[j], dp[j - nums[i]] + nums[i]);
        if (j == posSum && dp[posSum] == posSum){ // 如果出现posSum容量的书包已满,判断是否是新的方案,是则+1
            if (pre <= dp[j - nums[i]] + nums[i]) cnt++;
        }
    }
}

优化:

1、如果 target > sum,则不可能实现,方案数为0

2、如果 sum + target 为奇数,则不可能实现,方案数为0,因为需要容量恰好为 (sum + target) % 2 为奇数时,sum + target 除以2的真实值为小数,多个整数元素之和不可能为小数,例如sum + target = 7, (sum + target) * 1.0/ 2 = 3.5,需要元素之和为3.5才能使最后的表达式的值为target,但数组中的元素都是整数,不可能实现,因此方案数一定会为0。

3、如果posSum为负数,而元素之和不可能为负数,则方案数一定为0。


难点:

dp[j]代表的是恰好把容量为 j 的书包装满的方案数,而不是最大价值,如何确定递推关系?

方案:

对于0~ i 的元素,恰好装满容量为 j 的书包的方案数 = 没有nums[i]时恰好装满容量为 j 的书包的方案数(dp[j]) + 有nums[i]时恰好装满容量为 j - numms[i] 的书包的方案数(dp[j-nums[i]])。

重点:理解为什么一定包含第 i 个物品且恰好装满的方案数为dp[j - nums[i]]?

因为肯定包含第 i 个物品,所以需要为第 i 个物品腾出 nums[i] 大小的空间,则背包的容量就变成 j - nums[i],问题转换为将容量为 j - nums[i] 的书包恰好装满的方案数,即 dp[j - nums[i]]。

注意:

dp[0]应该初始化为1,即恰好装满容量为0的书包的方案数为1,即不装,否则后面计算的所有数值都是0。

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        for (int num : nums) sum += num;
        if (target > sum) return 0;
        if ((sum + target) % 2 != 0) return 0;
        int posSum = (sum + target) / 2;
        if (posSum < 0) return 0; // 元素值都>=0,求和不可能为负数,如果目标和为负数,直接返回0即可

        int dp[] = new int[posSum + 1];
        int cnt = 0;
        dp[0] = 1; // j = 0,书包容量为0,不放或者如果第一个元素刚好是0,则只有放这个方案
        for (int i = 0; i < nums.length; i++){
            for (int j = posSum; j >= nums[i]; j--){
                // 没有nums[i]时和为j的方案数(dp[j]) + 有nums[i]时和为j-numms[i]的方案数(dp[j-nums[i]])
                dp[j] += dp[j-nums[i]]; 
            }
        }
        return dp[posSum];
    }
}
  • 时间复杂度:O(M×N)
  • 空间复杂度:O(N)

【474.一和零】中等题

思路:

1、确定dp[i][j]的含义:

容量有两个维度,m和n,m是0的个数,n是1的个数,dp[i][j]表示最多有m个0和n个1的最大子集长度

2、确定递推关系:

不加第i个元素的最大子集长度 vs 加上第i个元素的最大子集长度,取长的作为答案。

3、确定初始值:

没有遍历字符串前,最大子集长度都是0即可

(难点:只是将一维滚动数组变成了二维滚动数组)

class Solution {
    public int findMaxForm(String[] strs, int m, int n) {
        // dp[i][j] -> 表示最多有m个0和n个1的最大子集长度
        int dp[][] = new int[m+1][n+1]; // 容量有两个维度,m和n,m是0的个数,n是1的个数
        for (String str : strs){
            // 计算当前字符串的0和1个数
            int cnt0 = 0;
            for (char c : str.toCharArray()){
                if (c == '0') cnt0++;
            }
            int cnt1 = str.length() - cnt0; // 不是0就是1,总长度-0的个数 = 1的个数

            // 滚动更新二维数组
            for (int i = m; i >= cnt0; i--){
                for (int j = n; j >= cnt1; j--){
                    // 不加第i个元素的最大子集长度 vs 加上第i个元素的最大子集长度,取长的作为答案
                    dp[i][j] = Math.max(dp[i][j], dp[i - cnt0][j - cnt1] + 1);
                }
            }
        }
        return dp[m][n];
    }
}
  • 时间复杂度:O(K×M×N),K指数组的长度
  • 空间复杂度:O(M×N)

  • 20
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值