给定一个非负整数数组,a1, a2, …, an, 和一个目标数,S。现在你有两个符号 +
和 -
。对于数组中的任意一个整数,你都可以从 +
或 -
中选择一个符号添加在前面。
返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
示例 1:
输入: nums: [1, 1, 1, 1, 1], S: 3
输出: 5
解释:
-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
一共有5种方法让最终目标和为3。
注意:
- 数组的长度不会超过20,并且数组中的值全为正数。
- 初始的数组的和不会超过1000。
- 保证返回的最终结果为32位整数。
解题思路
这个问题非常简单,我们首先想到的是通过回溯法解决这个问题。我们定义函数f(i, target)
表示[0:i]
这个区间内目标为target
的方法数,那么我们很容易得到下面这个表达式
f(i, target)=f(i-1, target-nums[i])+f(i-1, target+nums[i])
边界条件就是i==len(nums)
时,我们要判断target == 0
,如果是的话返回1
,否则0
。
class Solution:
def findTargetSumWays(self, nums, S):
"""
:type nums: List[int]
:type S: int
:rtype: int
"""
return self._findTargetSumWays(nums, 0, S)
def _findTargetSumWays(self, nums, index, target):
if index == len(nums):
if target == 0:
return 1
return 0
return self._findTargetSumWays(nums, index + 1, target-nums[index])\
+ self._findTargetSumWays(nums, index + 1, target+nums[index])
同样对于这种问题我们都可以通过记忆化搜索的方式去解决。这里要注意的细节就是,我们要记忆两种状态分别是index & target
。
class Solution:
def findTargetSumWays(self, nums, S):
"""
:type nums: List[int]
:type S: int
:rtype: int
"""
mem = dict()
return self._findTargetSumWays(nums, 0, S, mem)
def _findTargetSumWays(self, nums, index, target, mem):
if index == len(nums):
if target == 0:
return 1
return 0
tmp = "{},{}".format(index, target)
if tmp in mem:
return mem[tmp]
mem[tmp] = self._findTargetSumWays(nums, index + 1, target-nums[index], mem)\
+ self._findTargetSumWays(nums, index + 1, target+nums[index], mem)
return mem[tmp]
这个问题最简洁的思路时通过动态规划来解。这实际上是一个背包问题,在背包问题中,我们要考虑物品放还是不放,而在这个问题中我们要考虑是加上一个数还是减去一个数。此时的背包的大小应该可以容纳[-sum(nums),sum(nums)]
这个区间的所有数,而我们有len(nums)
个元素,所以我们最后需要一个(len(nums)+1)(2*sum_nums + 1)
大小的数组用来存储状态。传统背包问题可以用一个数组解决啊?着我们后面再说怎么用一维数组解决这个问题。接下来的过程很清晰,无非就是加上还是减去一个数的问题
mem[i][j] = mem[i-1][j+nums[i-1]] while j + nums[i-1] < 2*sum_nums + 1
mem[i][j] = mem[i-1][j-nums[i-1]] while j - nums[i-1] >= 0
最终代码如下
class Solution:
def findTargetSumWays(self, nums, S):
"""
:type nums: List[int]
:type S: int
:rtype: int
"""
sum_nums = sum(nums)
if sum_nums < S or -sum_nums > S:
return 0
len_nums = len(nums)
mem = [[0]*(2*sum_nums + 1) for _ in range(len_nums+1)]
mem[0][sum_nums] = 1
for i in range(1, len_nums + 1):
for j in range(2*sum_nums + 1):
if j + nums[i - 1] < 2*sum_nums + 1:
mem[i][j] += mem[i - 1][j + nums[i - 1]]
if j - nums[i - 1] >= 0:
mem[i][j] += mem[i - 1][j - nums[i - 1]]
return mem[len_nums][sum_nums + S]
我们怎么通过一维数组解决这个问题呢?实际上这个问题是之前Leetcode 416:分割等和子集(最详细的解法!!!) 提高,我们这里的问题同样可以理解为将nums
拆分为P&N
两个子集(P
做加法,N
做减法),那么我们的问题就变成了sum(P)-sum(N)=target
也就是2*sum(P)=target+sum(nums)
,也就是说target+sum(nums)
必须是一个偶数,这是一个非常重要的结论。我们通过这种思想结合动态规划,就可以写出下面的代码
class Solution:
def findTargetSumWays(self, nums, S):
"""
:type nums: List[int]
:type S: int
:rtype: int
"""
sum_nums = sum(nums)
if sum_nums < S or (S + sum_nums)%2 != 0:
return 0
target = (S + sum_nums) >> 1
mem = [0]*(target + 1)
mem[0] = 1
for num in nums:
for i in range(target, num-1, -1):
mem[i] += mem[i - num]
return mem[target]
数学与程序的完美融合,这也是很多黑客喜欢的编程方式。
reference:
我将该问题的其他语言版本添加到了我的GitHub Leetcode
如有问题,希望大家指出!!!