问题

给你一个整数数组 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

示例 2:
输入:nums = [1], target = 1
输出:1

提示:
1 <= nums.length <= 20
0 <= nums[i] <= 1000
0 <= sum(nums[i]) <= 1000
-1000 <= target <= 1000

思路

这道题目咋眼一看和动态规划背包啥的也没啥关系
本题要如何使表达式结果为target

既然为target,那么就一定有left组合 - right组合 = target
left + right = sum,而sum是固定的

公式来了, left - (sum - left) = target -> left = (target + sum)/2
target是固定的,sum是固定的,left就可以求出来
此时问题就是在集合nums中找出和为left的组合

回溯算法

  1. class Solution {
  2. List<List<Integer>> result = new ArrayList<>();
  3. List<Integer> path = new ArrayList<>();
  4. public void backtracking(int[] candidates, int target, int sum, int startIndex) {
  5. if (sum == target) {
  6. result.add(new ArrayList<Integer>(path));
  7. }
  8. // 如果 sum + candidates[i] > target 就终止遍历
  9. for (int i = startIndex; i < candidates.length && sum + candidates[i] <= target; i++) {
  10. sum += candidates[i];
  11. path.add(candidates[i]);
  12. backtracking(candidates, target, sum, i + 1);
  13. sum -= candidates[i];
  14. path.remove(path.size() - 1);
  15. }
  16. }
  17. public int findTargetSumWays(int[] nums, int S) {
  18. int sum = 0;
  19. for (int i = 0; i < nums.length; i++)
  20. sum += nums[i];
  21. if (S > sum)
  22. return 0; // 此时没有方案
  23. if ((S + sum) % 2)
  24. return 0; // 此时没有方案,两个int相加的时候要小心数值溢出的问题
  25. int bagSize = (S + sum) / 2; // 转变为组合总和问题,bagsize就是要求的和
  26. // 以下为回溯法代码
  27. Arrays.sort(nums); // 需要排序
  28. backtracking(nums, bagSize, 0, 0);
  29. return result.size();
  30. }
  31. }

动态规划

如何转化为01背包问题呢
假设添加“+”的元素总和为x,那么添加“-”对应的总和就是sum - xStarget
所以我们要求的是

  • x - (sum - x) = S
  • x = (S + sum) / 2

此时问题就转化为,装满容量为x背包,有几种方法

看到(S + sum) / 2 应该担心计算的过程中向下取整有没有影响
这么担心就对了,例如sum是5,S是2的话其实就是无解的,所以:

  1. if ((S + sum) % 2 == 1) return 0; // 此时没有方案

看到这种表达式,应该本能的反应,两个int相加数值可能溢出的问题,当然本题并没有溢出

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

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

    • dp[j]表示:填满j(包括j)这么大容积的包,有dp[j]种方法
    • 其实也可以使用二维dp数组来求解本题,dp[i][j]:使用下标为[0, i]nums[i]能够凑满j(包括j)这么大容量的包,有dp[i][j]种方法
  • 确定递推公式

    • 有哪些来源可以推出dp[j]呢?
    • 不考虑nums[i]的情况下,填满容量为j - nums[i]的背包,有dp[j - nums[i]]种方法
    • 那么只要搞到nums[i]的话,凑成dp[j]就有dp[j - nums[i]]种方法

那么需要把这些方法累加起来就可以了,dp[i] += dp[j - nums[i]]
所以求组合类问题的公式,都是类似这种:

  1. dp[j] += dp[j - nums[i]]
  • 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]]推导出来
  • 确定遍历顺序

    • nums放在外循环,target在内循环,且内循环倒序
  • 举例推导dp数组

    • 输入:nums: [1, 1, 1, 1, 1], S: 3
    • bagSize = (S + sum) / 2 = (3 + 5) / 2 = 4

640 (2).webp

  1. class Solution {
  2. public int findTargetSumWays(int[] nums, int target) {
  3. int sum = 0;
  4. for (int i = 0; i < nums.length; i++)
  5. sum += nums[i];
  6. if (target > sum) return 0; // 此时没有方案
  7. if ((target + sum) % 2 == 1) return 0; // 此时没有方案
  8. int bagSize = (target + sum) / 2;
  9. int[] dp = new int[bagSize + 1];
  10. dp[0] = 1;
  11. for (int i = 0; i < nums.length; i++) {
  12. for (int j = bagSize; j >= nums[i]; j--) {
  13. dp[j] += dp[j - nums[i]];
  14. }
  15. }
  16. return dp[bagSize];
  17. }
  18. }
  • 时间复杂度O(n * m),n为正数个数,m为背包容量
  • 空间复杂度:O(m) m为背包容量