题目
代码
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
}
};
方法一:DFS 深度优先搜索
分析
题目中可以选择向数组中的每个整数前添加 + + + 或 − - −,所以可以看作当前节点有且只有两条通向支路,只要统计所有结果等于 t a r g e t target target 的路径数量即可。
代码
class Solution {
private:
int dfs(vector<int>& nums, int& target, int n, int cur) {
if(n == nums.size()) return cur == target ? 1 : 0;
int addition = dfs(nums, target, n + 1, cur + nums[n]);
int subtraction = dfs(nums, target, n + 1, cur - nums[n]);
return addition + subtraction;
}
public:
int findTargetSumWays(vector<int>& nums, int target) {
return dfs(nums, target, 0, 0);
}
};
复杂度分析
- 时间复杂度: O ( 2 n ) O(2^n) O(2n),其中 n n n 是数组 n u m s nums nums 的长度,算法需要遍历所有情况。
- 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 n u m s nums nums 的长度。空间复杂度主要取决于递归调用的栈空间,栈的深度不超过 n n n。
方法二:记忆化搜索
分析
在方法一的基础上,我们可以将 DFS 的函数中的可变参数 n n n 和 c u r cur cur 记录下来作为记忆化容器的两个维度,返回值作为记忆化容器的记录值,以此来减少重复计算过程。
代码
class Solution {
private:
unordered_map<string, int> cache;
int dfs(vector<int>& nums, int& target, int n, int cur) {
string key = to_string(n) + '_' + to_string(cur);
if(cache.count(key)) return cache[key];
if(n == nums.size()) {
cur == target ? cache[key] += 1 : cache[key] += 0;
return cache[key];
}
int addition = dfs(nums, target, n + 1, cur + nums[n]);
int subtraction = dfs(nums, target, n + 1, cur - nums[n]);
cache[key] = addition + subtraction;
return cache[key];
}
public:
int findTargetSumWays(vector<int>& nums, int target) {
return dfs(nums, target, 0, 0);
}
};
复杂度分析
- 时间复杂度: O ( 2 n ) O(2^n) O(2n),其中 n n n 是数组 n u m s nums nums 的长度,算法需要遍历所有情况。
- 空间复杂度: O ( n ) O(n) O(n),其中 n n n 是数组 n u m s nums nums 的长度。空间复杂度主要取决于递归调用的栈空间,栈的深度不超过 n n n。
方法三:二维动态规划
分析
假设数组的元素和为
s
u
m
sum
sum,添加
+
+
+ 号的元素之和为
a
a
a,添加
−
-
− 号的元素之和为
b
b
b,得到的表达式的结果为:
a
+
b
=
s
u
m
a
−
b
=
t
a
r
g
e
t
\begin{aligned} & a+b=sum \\ & a-b=target \end{aligned}
a+b=suma−b=target 因此我们可以得到:
b
=
s
u
m
−
t
a
r
g
e
t
2
,
b
=
2
n
,
n
∈
N
b=\dfrac{sum-target}{2},\ b = 2n,\ n \in N
b=2sum−target, b=2n, n∈N 即
b
b
b 为自然偶数,因此可以将问题转化成能在数组中抽取元素组成和为
b
b
b 的子集的个数。设定二维数组
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j] 表示在前
i
i
i 个元素中,组成和为
j
j
j 的子集的个数。
当没有任何元素可以选取时,元素和只能为
0
0
0,对应的方案数是
1
1
1, 因此在二维数组
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j] 中,
d
p
[
0
]
[
0
]
dp[0][0]
dp[0][0] 值为
1
1
1,其余起始值均为
0
0
0。对于第
i
i
i 个元素有选和不选两种情况:
- 如果 j ≥ n u m s [ i ] j \geq nums[i] j≥nums[i] 时,如果不选 n u m s [ i ] nums[i] nums[i],子集数是 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j];如果选 n u m s [ i ] nums[i] nums[i],子集数是 d p [ i − 1 ] [ j − n u m s [ i ] ] dp[i-1][j-nums[i]] dp[i−1][j−nums[i]],因此总方案数为: d p [ i − 1 ] [ j ] + d p [ i − 1 ] [ j − n u m s [ i ] ] dp[i-1][j]+dp[i-1][j-nums[i]] dp[i−1][j]+dp[i−1][j−nums[i]]。
- 如果 j < n u m s [ i ] j < nums[i] j<nums[i] 时,则不能选 n u m s [ i ] nums[i] nums[i],子集数是 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j]。
综上所述
d
p
[
i
]
[
j
]
[
k
]
dp[i][j][k]
dp[i][j][k] 的状态转移方程可表示为:
d
p
[
i
]
[
j
]
=
{
d
p
[
i
−
1
]
[
j
]
+
d
p
[
i
−
1
]
[
j
−
n
u
m
s
[
i
]
]
i
f
j
≥
n
u
m
s
[
i
]
d
p
[
i
−
1
]
[
j
]
i
f
j
<
n
u
m
s
[
i
]
dp[i][j] = \begin{cases} dp[i-1][j]+dp[i-1][j-nums[i]] &if\ j \geq nums[i]\\ dp[i-1][j] &if\ j < nums[i] \end{cases}
dp[i][j]={dp[i−1][j]+dp[i−1][j−nums[i]]dp[i−1][j]if j≥nums[i]if j<nums[i]
d
p
[
n
]
[
b
]
dp[n][b]
dp[n][b] 即为问题答案。
代码
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = accumulate(nums.begin(), nums.end(), 0);
if (sum - target < 0 || (sum - target) % 2 != 0) return 0;
int b = (sum - target) / 2, len = nums.size();
vector<vector<int>> dp (len + 1, vector<int>(b + 1, 0));
dp[0][0] = 1;
for(int i = 1; i <= len; ++i) {
for(int j = 0; j <= b; ++j) {
if(j >= nums[i - 1]) {
dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i - 1]];
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[len][b];
}
};
复杂度分析
- 时间复杂度: O ( n × b ) O(n \times b) O(n×b),其中 n n n 是数组长度, b b b 是负值元素和。
- 空间复杂度: O ( n × b ) O(n \times b) O(n×b),其中 n n n 是数组长度, b b b 是负值元素和,实现动态规划需要创建 n × b n \times b n×b 的二维数组 d p dp dp。
方法四:一维动态规划
分析
根据方法三的状态转移方程可知,更新
d
p
[
i
]
[
]
dp[i][]
dp[i][] 的每个元素值时,只依赖于
d
p
[
i
−
1
]
[
]
dp[i-1][]
dp[i−1][] 的元素值。因此,可以使用一维滚动数组
d
p
[
j
]
dp[j]
dp[j] 来替代二维数组
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j] 对方法三进行空间上的优化。
d
p
[
j
]
dp[j]
dp[j] 的状态转移方程可表示为:
d
p
[
j
]
=
d
p
[
j
]
+
d
p
[
j
−
n
u
m
s
[
i
]
]
dp[j] = dp[j]+dp[j-nums[i]]
dp[j]=dp[j]+dp[j−nums[i]]
d
p
[
b
]
dp[b]
dp[b] 即为问题答案。
要点
根据方法三的状态转移方程,实现时内层循环需采用倒序遍历的方式更新元素值。
代码
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = accumulate(nums.begin(), nums.end(), 0);
if (sum - target < 0 || (sum - target) % 2 != 0) return 0;
int b = (sum - target) / 2;
vector<int> dp (b + 1, 0);
dp[0] = 1;
for(auto num : nums) {
for(int j = b; j >= 0; --j) {
if(j >= num) {
dp[j] = dp[j] + dp[j - num];
}
}
}
return dp[b];
}
};
复杂度分析
- 时间复杂度: O ( n × b ) O(n \times b) O(n×b),其中 n n n 是数组长度, b b b 是负值元素和。
- 空间复杂度: O ( b ) O(b) O(b),其中 b b b 是负值元素和,实现动态规划需要创建 b b b 的一维滚动数组 d p dp dp。