动态规划进阶05:⽬标和

86 阅读3分钟

⽬标和

力扣494. 目标和 - 力扣(LeetCode)
给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 '+' 或 '-' ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 '+' ,在 1 之前添加 '-' ,然后串联起来得到表达式 "+2-1" 。 返回可以通过上述方法构造的、运算结果等于 target 的不同表达式的数目。
示例 1:
输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 3 。
-1 + 1 + 1 + 1 + 1 = 3
+1 - 1 + 1 + 1 + 1 = 3
+1 + 1 - 1 + 1 + 1 = 3
+1 + 1 + 1 - 1 + 1 = 3
+1 + 1 + 1 + 1 - 1 = 3
提示: 1 <= nums.length <= 20
0 <= nums[i] <= 1000
0 <= sum(nums[i]) <= 1000
-1000 <= target <= 1000
如何转化为01背包问题呢。假设数组总和为sum加法的总和为x,那么减法对应的总和就是sum - x。
所以我们要求的是 x - (sum - x) = S
即:x = (S + sum) / 2
此时问题就转化为,装满容量为x背包,有⼏种⽅法。⼤家看到(S + sum) / 2 应该担⼼计算的过程中向下取整有没有影响。这么担⼼就对了,例如sum 是5,S是2的话其实就是⽆解的,所以:

    if((sun + target) % 2 == 1)
            return 0;

还有当|S| > sun也是无解的这种情况就不用过多解释了吧所以:

    if(target > sun || -target > sun)
            return 0;

再回归到01背包问题,为什么是01背包呢?因为每个物品(题⽬中的1)只⽤⼀次!这次和之前遇到的背包问题不⼀样了,之前都是求容量为j的背包,最多能装多少。本题则是装满有⼏种⽅法。其实这就是⼀个组合问题了。

动规五部曲:

1. 确定dp数组以及下标的含义

dp[j] 表示:填满j(包括j)这么⼤容积的包,有dp[i]种⽅法其实也可以使⽤⼆维dp数组来求解本题,dp[i][j]:使⽤ 下标为[0, i]的nums[i]能够凑满j(包括j)这么⼤容量的包,有dp[i][j]种⽅法。下⾯我都是统⼀使⽤⼀维数组进⾏讲解, ⼆维降为⼀维(滚动数组),其实就是上⼀层拷⻉下来,这个我在动态规划进阶02:01背包理论基础(滚动数组) - 掘金 (juejin.cn)也有介绍。

2. 确定递推公式

有哪些来源可以推出dp[j]呢?不考虑nums[i]的情况下,填满容量为j - nums[i]的背包,有dp[j - nums[i]]中⽅法。
那么只要搞到nums[i]的话,凑成dp[j]就有dp[j - nums[i]] 种⽅法。
举⼀个例⼦:填满背包容量为3有dp[3]种⽅法。那么只需要有⼀个nums[i] = 2,有dp[3]⽅法可以凑⻬容量为3的背包,相应的就有多少种⽅法可以凑⻬容量为dp[5]的背包。那么需要把这些⽅法累加起来就可以了,dp[5] += dp[5 - 5]所以求组合类问题的公式,都是类似这种:

dp[j] += dp[j - nums[i]]

3. dp数组如何初始化

从递归公式可以看出,在初始化的时候dp[0] ⼀定要初始化为1,因为dp[0]是在公式中⼀切递推结果的起源,如果dp[0]是0的话,递归结果将都是0。
dp[0] = 1,理论上也很好解释,装满容量为0的背包,有1种⽅法,就是装0件物品。
dp[j]其他下标对应的数值应该初始化为0,从递归公式也可以看出,dp[j]要保证是0的初始值,才能正确的由dp[j - nums[i]]推导出来。

4. 确定遍历顺序

对于01背包问题⼀维dp的遍历前面的文章我们都讲过,nums放在外循环,target在内循环,且内循环倒序。

5. 举例推导dp数组

输⼊:nums: [1, 1, 1, 1, 1], S: 3
bagSize = (S + sum) / 2 = (3 + 5) / 2 = 4
dp数组状态变化如下:

image.png
如果对结果不理解,回顾一下递推公式和遍历顺序。
以上分析完毕,Java代码如下:

class Solution {
    public int findTargetSumWays(int[] nums, int target) {
        int l = nums.length;
        int sun = 0;
        for(int i : nums)
            sun += i;
        if(target > sun || -target > sun)
            return 0;
        if((sun + target) % 2 == 1)
            return 0;    
        int t = (sun + target) / 2;
        int[] dp = new int[t + 1];
        dp[0] = 1;
        for(int i = 0; i < l; i++){
            for(int j = t; j >= nums[i]; j--){
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[t];
    }
}