问题
给你一个整数数组 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
的组合
回溯算法
class Solution {
List<List<Integer>> result = new ArrayList<>();
List<Integer> path = new ArrayList<>();
public void backtracking(int[] candidates, int target, int sum, int startIndex) {
if (sum == target) {
result.add(new ArrayList<Integer>(path));
}
// 如果 sum + candidates[i] > target 就终止遍历
for (int i = startIndex; i < candidates.length && sum + candidates[i] <= target; i++) {
sum += candidates[i];
path.add(candidates[i]);
backtracking(candidates, target, sum, i + 1);
sum -= candidates[i];
path.remove(path.size() - 1);
}
}
public int findTargetSumWays(int[] nums, int S) {
int sum = 0;
for (int i = 0; i < nums.length; i++)
sum += nums[i];
if (S > sum)
return 0; // 此时没有方案
if ((S + sum) % 2)
return 0; // 此时没有方案,两个int相加的时候要小心数值溢出的问题
int bagSize = (S + sum) / 2; // 转变为组合总和问题,bagsize就是要求的和
// 以下为回溯法代码
Arrays.sort(nums); // 需要排序
backtracking(nums, bagSize, 0, 0);
return result.size();
}
}
动态规划
如何转化为01背包问题呢
假设添加“+”的元素总和为x
,那么添加“-”对应的总和就是sum - x
,S
是target
所以我们要求的是
- x - (sum - x) = S
- x = (S + sum) / 2
此时问题就转化为,装满容量为x背包,有几种方法
看到(S + sum) / 2
应该担心计算的过程中向下取整有没有影响
这么担心就对了,例如sum是5,S是2的话其实就是无解的,所以:
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]]
所以求组合类问题的公式,都是类似这种:
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
- 输入:
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sum = 0;
for (int i = 0; i < nums.length; i++)
sum += nums[i];
if (target > sum) return 0; // 此时没有方案
if ((target + sum) % 2 == 1) return 0; // 此时没有方案
int bagSize = (target + sum) / 2;
int[] dp = new int[bagSize + 1];
dp[0] = 1;
for (int i = 0; i < nums.length; i++) {
for (int j = bagSize; j >= nums[i]; j--) {
dp[j] += dp[j - nums[i]];
}
}
return dp[bagSize];
}
}
- 时间复杂度O(n * m),n为正数个数,m为背包容量
- 空间复杂度:O(m) m为背包容量