【LeetCode笔记】494. 目标和(Java、动态规划、背包问题、滚动数组)

打卡第十五天~继续加油!

题目描述

  • 和上一道分割等和子集的做法很像
  • 这里代码迭代了很多次,但是时间复杂度是一样的,只是空间复杂度、耗时不断优化。
    在这里插入图片描述
    在这里插入图片描述

思路 && 代码

1. 动态规划 O( n 2 n^2 n2)、O( n 2 n^2 n2)(最方便理解,初版)

  • dp[i][j]:下标[0 ~ i]构成的数集,能得到 j - sum 的情况种数
  • 因为nums[ ]可构成元素范围为[-sum, sum],因此开出[2 * sum + 1]的列数组。
    其中[0] 代表 -sum,[2 * sum] 代表 [sum],以此类推。
  • 注意:初始化时,nums[0] 可能等于 -nums[0],因此要用到 +=,而非 = 。
  • 状态转移:对于当前的 j,分成 +nums[i],和-nums[i]的情况,对上一行的值进行选取即可。
  • 缺陷:空间复杂度不方便通过滚动数组的方式进行优化,因为状态转移方程的过程中,不但用到了前面的元素,还用到的后面的元素。解决方法:转换成01背包问题
class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        for(int temp : nums) {
            sum += temp;
        }
        // 全正 or 全负,不在范围的情况
        if(sum < target || -sum > target) {
            return 0;
        }
        
        // dp[i][j]:下标[0 ~ i]构成的数集,能得到 j - sum 的情况种数
        int top = 2 * sum + 1;
        int[][] dp = new int[nums.length][top];
        // 初始化:只取第一个元素,只能给 nums[0] 和 -nums[0] 带来 1 个种数
        dp[0][sum + nums[0]] = 1;
        // 注意:存在 nums[0] == -nums[0] 的情况,因此这边要用 +=
        dp[0][sum - nums[0]] += 1;

        // 状态转移
        for(int i = 1; i < nums.length; i++) {
            for(int j = 0; j < top; j++) {
                // Case 1:第 i 个元素取 + 
                if(j >= nums[i]) {
                    dp[i][j] = dp[i - 1][j - nums[i]];
                }
                // Case 2: 第 i 个元素取 -
                if(j + nums[i] < top) {
                    dp[i][j] += dp[i - 1][j + nums[i]];
                }
            }
        }
        return dp[nums.length - 1][target + sum];
    }
}

2. 转换成 01 背包问题 O( n 2 n^2 n2)、O( n n n)

  • 实际上,题目可以这样转换成01背包问题:把 - 当成 0,不选;把 + 当成 1,选。
  • 原本的(-nums) + (+nums) == target,表达式左边和右边都加上 sum,就转换成
    0 + 2 * (+nums) == sum + target,方便起见,我们可以再进行除2操作,变成
    +nums == (sum + target) / 2
  • 注意:由此可推出,如果(sum + target) 为奇数,说明不存在对应的 +nums 序列,也就是不可取。
  • 接下来就可以正常地进行 01背包 的动态规划了~
  • 滚动数组:逆序,现在没有减法的情况下,可以保证无后效性
class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        for(int temp : nums) {
            sum += temp;
        }
        // 不在范围的情况 && 奇数无法匹配到选取方式(可证)
        if(sum < target || (sum + target) % 2 == 1) {
            return 0;
        }

        // 转换成 01背包:-号转成不取;+号转成取,两倍
        // 实际上,只要考虑到 target + sum 即可,后面的和结果无关
        int top = (sum + target) / 2 + 1;

        // dp[i][j]:下标[0 ~ i]构成的数集,能得到 j - sum 的情况种数
        int[] dp = new int[top];
        // 初始化:第一个元素不取,只能给 0 带来 1 个种数
        dp[0] = 1;

        // 状态转移
        for(int i = 0; i < nums.length; i++) {
            for(int j = top - 1; j >= nums[i]; j--) {
                // Case 1:取第 i 个元素
                dp[j] += dp[j - nums[i]];
                // Case 2: 不取第 i 个元素(一维情况下相当于不用考虑)
            }
        }
        return dp[top - 1];
    }
}
  • 来个无注释版代码吧,方便直接看代码:
class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int sum = 0;
        for(int temp : nums) {
            sum += temp;
        }
        if(sum < target || (sum + target) % 2 == 1) {
            return 0;
        }

        int top = (sum + target) / 2 + 1;
        int[] dp = new int[top];
        dp[0] = 1;

        for(int i = 0; i < nums.length; i++) {
            for(int j = top - 1; j >= nums[i]; j--) {
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[top - 1];
    }
}

二刷

离谱!添加了测试用例,上面的代码需要添加负数条件了(见下面的代码)
class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        // 转换成背包:+取两次,-不取。target 相当于加了一次 sum
        for(int temp : nums) {
            target += temp;
        }
        // 偶数之和不能为奇数 || 非负数之和不能为负
        if(target % 2 == 1 || target < 0) {
            return 0;
        }
        target /= 2;
        int[] dp = new int[target + 1];
        // 边界处理:0的组合法有一个(都不取)
        dp[0] = 1;
        for(int i = 0; i < nums.length; i++) {
            for(int j = target; j >= nums[i]; j--) {
                // 相当于,这一轮的结果 = 上一轮的结果 + 这一轮的添加
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[target];
    }
}
  • 无注释版,11行有效代码
class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        for(int temp : nums) {
            target += temp;
        }
        if(target % 2 == 1 || target < 0) {
            return 0;
        }
        target /= 2;
        int[] dp = new int[target + 1];
        dp[0] = 1;
        for(int i = 0; i < nums.length; i++) {
            for(int j = target; j >= nums[i]; j--) {
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[target];
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值