力扣-494. 目标和
1.题目
题目地址(494. 目标和 - 力扣(LeetCode))
https://leetcode.cn/problems/target-sum/
题目描述
给你一个非负整数数组 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
2.题解
这里每个数仅能选择一次,是一个0/1背包问题
2.1 数学+动态规划(一维dp数组)
思路
首先我们如果思考想将这一题转换为一个动态规划问题,或者说一个0-1背包问题
我们必须明确选/不选什么类型的元素,这里有要加的元素/要减的元素两种,那么我们有必要设计一个二维dp数组,分别来记录这两种元素吗?
答案是没有必要的,由于+元素/-元素和target之间必然存在某种数学关系,我们可以通过这种数学关系来简化计算
首先我们假设sum = 所有元素的和(当作unsigned,无论符号),那么我们假设-元素和(这里的和为单纯的元素和)为neg, +元素便为sum - neg
target = +元素 - (-元素)= sum - neg - neg = sum - 2*neg
neg = (sum - target) / 2
其中sum很好求,我们简化使用accumulate函数,注意一共三个参数:数组的首位迭代器和初始值
接下来我们便可以利用动态规划来求解能组成元素和为neg的组合了
dp[i] = (在[0,i]个元素中任意选择元素,和为...), 如果我们使用这种传统思维,便会发现其中的局限性,如何在和为茫茫dp数组中寻找和为neg的那个组合?
我们逆转思路,dp[i]=(表示和为i的组合个数),且每一次新的组合都可以是基于前面已经列出组合 + 一个新元素 = 组成的新组合, 也就是有了递推关系
且最后我们寻找和为neg的组合个数时,直接求dp[neg]即可
之前我们的 查找条件(在索引[0,i]之间寻找,), 查找结果(根据题目而定)
这里还是遵循 查找条件(和为neg), 查找结果(组合个数)
代码1(使用二维dp)
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = 0;
for (int& num : nums) {
sum += num;
}
int diff = sum - target;
if (diff < 0 || diff % 2 != 0) {
return 0;
}
int n = nums.size(), neg = diff / 2;
vector<vector<int>> dp(n + 1, vector<int>(neg + 1));
dp[0][0] = 1;
for (int i = 1; i <= n; i++) {
int num = nums[i - 1];
for (int j = 0; j <= neg; j++) {
dp[i][j] = dp[i - 1][j];
if (j >= num) {
dp[i][j] += dp[i - 1][j - num];
}
}
}
return dp[n][neg];
}
};
代码2(简化,使用一维dp)
- 语言支持:C++
C++ Code:
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int n = nums.size();
int sum = accumulate(nums.begin(), nums.end(), 0);
if(sum - target < 0 || (sum - target) & 1) return 0;
int neg = (sum - target) / 2;
vector<int> dp(neg + 1);
dp[0] = 1; // 什么都不选是一种选择方式
for(int i = 0; i < n; i++){
int num = nums[i];
// 为了避免覆盖,使用倒序更新
for(int j = neg; j >= num; j--){
dp[j] += dp[j - num];
}
}
return dp[neg];
}
};
复杂度分析
-
时间复杂度:\(O(n\times(sum-target))\),其中\(n\)是数组nums的长度,sum是数组nums的元素和,target是目标数。动态规划有\((n+1)\times(\frac{sum-target}2+1)\)个状态,需要计算每个状态的值。
-
空间复杂度:\(O(sum-target)\),其中sum是数组nums的元素和,target是目标数。使用空间优化的实现,需要创建长度为\(\frac{sum-target}2+1\)的数组\(dp\) 。
2.2 回溯
思路
要么加,要么减,我们可以考虑利用回溯遍历所有可能性,在一种可能性结束后,进行回溯,执行另一种可能性。
代码
class Solution {
public:
int count = 0;
int findTargetSumWays(vector<int>& nums, int target) {
backtrack(nums, target, 0, 0);
return count;
}
void backtrack(vector<int> &nums, int target, int index, int sum){
if(index == nums.size()){
if(sum == target){
count++;
}
}else{
backtrack(nums, target, index + 1, sum + nums[index]);
backtrack(nums, target, index + 1, sum - nums[index]);
}
}
};
复杂度分析
-
时间复杂度:\(O(2^n)\),其中\(n\)是数组nums的长度。回溯需要遍历所有不同的表达式,共有\(2^n\)种不同的表达式,每种表达式计算结果需要\(O(1)\)的时间,因此总时间复杂度是\(O(2^n)\)。
-
空间复杂度:\(O(n)\),其中\(n\)是数组nums的长度。空间复杂度主要取决于递归调用的栈空间,栈的深度不超过\(n\)。