题目描述
You are given a list of non-negative integers, a1, a2, …, an, and a target, S. Now you have 2 symbols +
and -
. For each integer, you should choose one from +
and -
as its new symbol.
Find out how many ways to assign symbols to make sum of integers equal to target S.
Example 1:
Input: nums is [1, 1, 1, 1, 1], S is 3.
Output: 5
Explanation:
-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
There are 5 ways to assign symbols to make the sum of nums be target 3.
Note:
- The length of the given array is positive and will not exceed 20.
- The sum of elements in the given array will not exceed 1000.
- Your output answer is guaranteed to be fitted in a 32-bit integer.
题目分析与解决方案
题目的意思大致就是数组中的每个数都可以是正数或者负数,将数组中的所有正数和负数求和,看有多少种组合能满足求和的结果等于target S。
法一:暴力递归求解
最容易想到的方法就是暴力求解,直接使用递归遍历所有可能的组合,从中挑出满足求和等于target S的组合。
相当于遍历这一棵二叉树,树的高度为N=nums.size()
,遍历的总操作数
1
+
2
+
4
+
…
…
+
2
n
1+2+4+……+2^n
1+2+4+……+2n,时间复杂度为
O
(
2
n
)
O(2^n)
O(2n)。相关代码就是代码部分的法一。这个方法容易理解,也容易实现,代码较少,就是效率不咋地。
法二:目标转移+动态规划
这个方法比较难想,我是在大神的博客上看到的。我感觉最起码得对动态规划比较熟悉,而且可能做过相类似的题才可能想得到。
首先设P为nums中的所有正数,N为nums中的所有负数,于是有
s
u
m
(
P
)
−
s
u
m
(
N
)
=
s
u
m
(
n
u
m
s
)
,
s
u
m
(
P
)
+
s
u
m
(
N
)
=
t
a
r
g
e
t
sum(P)-sum(N)=sum(nums),sum(P)+sum(N)=target
sum(P)−sum(N)=sum(nums),sum(P)+sum(N)=target
两式相加,得到
2 ∗ s u m ( P ) = s u m ( n u m s ) + t a r g e t = > s u m ( P ) = ( s u m ( n u m s ) + t a r g e t ) / 2 2*sum(P)=sum(nums)+target \\ =>sum(P)=(sum(nums)+target)/2 2∗sum(P)=sum(nums)+target=>sum(P)=(sum(nums)+target)/2
根据上述式子可知,如果sum(nums)+target不为偶数(2*整数的结果必然等于偶数),或者sum(nums)<target(这种情况下nums中的元素全为正,求和都小于target,无解),则必然无解。
所以接下来的目标就是
s
u
m
(
P
)
=
(
s
u
m
(
n
u
m
s
)
+
t
a
r
g
e
t
)
/
2
sum(P)=(sum(nums)+target)/2
sum(P)=(sum(nums)+target)/2,也就是在数组中找到任意的数,组合起来之后求和等于
(
s
u
m
(
n
u
m
s
)
+
t
a
r
g
e
t
)
/
2
(sum(nums)+target)/2
(sum(nums)+target)/2。(只需考虑他们为正数的情况,也就是只考虑+
的情况,因为求和的是
s
u
m
(
P
)
sum(P)
sum(P),也就是sums中的正数部分)
因为
(
s
u
m
(
n
u
m
s
)
+
t
a
r
g
e
t
)
/
2
(sum(nums)+target)/2
(sum(nums)+target)/2是一个常数,设为new_target
。所以求解目标就变为在数组中找到任意的数,组合起来之后求和等于new_target
。这个过程可以考虑用动态规划来解,不然我感觉也没啥方法能解出来,暴力求解时间复杂度太高了。
设dp[i]
等于求和为i的组合的数量,例如dp[5]
等于数组中能够组合起来求和为5的组合的数量。接下来就是要研究状态转移方程。
设当前在数组中遍历到的元素为cur
,则dp[j]
必然要加上dp[j-cur]
。因为可以这么理解:当前元素为cur
,那么j=j-cur+cur
,所以组合求和为j
的数量必然要加上组合求和为j-cur
的数量。有多少种组合求和为j-cur
,就需要加上多少种组合求和为j
,因为当前元素为cur
。为什么是加上呢?因为本来就有多种组合能满足求和为j
,这个组合的数目就是dp[j]
。所以状态转移方程:
d
p
[
j
]
=
d
p
[
j
]
+
d
p
[
j
−
c
u
r
]
dp[j] = dp[j] + dp[j-cur]
dp[j]=dp[j]+dp[j−cur]
例如:
设
c
u
r
=
3
,
j
=
9
,
则
:
d
p
[
9
]
=
d
p
[
9
]
+
d
p
[
9
−
3
]
d
p
[
8
]
=
d
p
[
8
]
+
d
p
[
8
−
3
]
d
p
[
7
]
=
d
p
[
7
]
+
d
p
[
7
−
3
]
…
…
…
…
…
…
d
p
[
3
]
=
d
p
[
3
]
+
d
p
[
3
−
3
]
设cur=3,j=9,则: \\ dp[9] = dp[9] + dp[9-3]\\ dp[8] = dp[8] + dp[8-3]\\ dp[7] = dp[7] + dp[7-3]\\ \dots\dots \\ \dots\dots \\ \dots\dots \\ dp[3] = dp[3]+dp[3-3]
设cur=3,j=9,则:dp[9]=dp[9]+dp[9−3]dp[8]=dp[8]+dp[8−3]dp[7]=dp[7]+dp[7−3]………………dp[3]=dp[3]+dp[3−3]
至于更新的顺序,应该是i=nums.size()--->i=cur
,而不是i=cur--->i=nums.size()
。为什么呢?其实可以自己算一算,如果从低到高的顺序来更新的话,大的结果出现冗余,会导致最终的结果大于正确的结果。因为更新的话,其实是用上一步的低dp
加上当前的cur
,才能更新高dp
。如果从低到高来更新,则是利用这一步的低dp
来更新高dp
,那么必然存在冗余。
例如:设 n u m s = [ 1 ] nums=[1] nums=[1]。则初始状态 d p [ 0 ] = 1 , d p [ 1 ] = 0 , d p [ 2 ] = 0 dp[0]=1,dp[1]=0,dp[2]=0 dp[0]=1,dp[1]=0,dp[2]=0。接着遍历数组,cur=1。
如果从低至高更新:
d
p
[
1
]
=
d
p
[
1
]
+
d
p
[
1
−
1
]
=
1
d
p
[
2
]
=
d
p
[
2
]
+
d
p
[
2
−
1
]
=
1
(
错
误
,
实
际
上
数
组
中
只
有
1
,
求
和
不
可
能
为
2
)
dp[1] = dp[1] + dp[1-1] = 1\\ dp[2] = dp[2] + dp[2-1] = 1\\ (错误,实际上数组中只有1,求和不可能为2)
dp[1]=dp[1]+dp[1−1]=1dp[2]=dp[2]+dp[2−1]=1(错误,实际上数组中只有1,求和不可能为2)
如果从高至低更新:
d
p
[
2
]
=
d
p
[
2
]
+
d
p
[
2
−
1
]
=
0
d
p
[
1
]
=
d
p
[
1
]
+
d
p
[
1
−
1
]
=
1
(
很
明
显
,
这
才
是
正
确
的
结
果
)
dp[2] = dp[2] + dp[2-1] = 0\\ dp[1] = dp[1] + dp[1-1] = 1\\ (很明显,这才是正确的结果)
dp[2]=dp[2]+dp[2−1]=0dp[1]=dp[1]+dp[1−1]=1(很明显,这才是正确的结果)
提交代码
class Solution {
public:
// 法一,直接暴力递归+回溯,尝试所有组合的值,效率较低
// int findTargetSumWays(vector<int>& nums, int S) {
// if(nums.size() == 0)
// return 0;
// return dfs(nums, 0, (long long)S);
// }
// int dfs(vector<int>& nums, int cur_index, long long remain) {
// if (cur_index == nums.size())
// if (remain == 0)
// return 1;
// else
// return 0;
// int cur = nums[cur_index];
// return dfs(nums, cur_index+1, remain-cur) + dfs(nums, cur_index+1, remain+cur);
// }
// 法二,动态规划进行优化
int findTargetSumWays(vector<int>& nums, int S) {
if(nums.size() == 0)
return 0;
int sum = 0;
// 先对数组求和
for (int i = 0; i < nums.size(); i++)
sum += nums[i];
int target = S;
// 设P为nums中的正数,N为nums中的负数,sum(P)-sum(N)=sum(nums)
// sum(P)+sum(N)=target
// 两式相加,得到2*sum(P)=sum(nums)+target==>sum(p)=(sum(nums)+target)/2
// 根据原式可知,如果sum+target不为偶数,或者sum<target,则必然无解
// 接下来的目标就是在数组中找到任意的数,组合起来求和等于(sum(nums)+target)/2
// 这个过程可以用动态规划来解。dp[i]即求和为i的方案数量
// 比如dp[5]等于数组中能够组合起来求和为5的组合的数量
// 状态转移方程就是dp[i] = dp[i] + dp[i-cur],cur为在数组中当前遍历的数
if (sum < target || (sum+target) % 2 != 0)
return 0;
int new_target = (sum + target)/2;
int dp[new_target+1] = {0};
dp[0] = 1;
for (int cur : nums) {
for (int i = new_target; i >= cur; i--) {
dp[i] += dp[i-cur];
}
}
return dp[new_target];
}
};
谢谢大家的阅读~~如果文中出现什么错误,欢迎指出,谢谢!