494. 目标和
494. 目标和
问题描述
给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。
返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
示例:
输入: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 位整数存下。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/target-sum
解答
这题是在练习01背包找的题目,先入为主的就拿背包问题来做:
\[f(i, j) = f(i-1, j - num[i]) + f(i - 1, j + num[i])
\]
\(f(i, j )\)是状态转移方程,将i个物品放入空间为j的背包的次数,因为可以存在加减法,所以为上述式子,但是在计算\(f(i, j)\)时\(f(i-1, j+num[i])\)是没有计算出来的。在查看别人的计算方法中,还想仍然有这样可以做出来的 ,但是这里就不考虑了。
正确且方便的方法是,先观察题目,如果相加的数字和为p
,相减的数字和为m
,那么可以得到
\[p + m = sum(nums) \\
p - m = S \\
\therefore p = \frac{sum(nums) + S}{2}
\]
所以只需要在所有数字中,找到部分数字,让其和为 \(\frac{sum(nums) +S}{2}\),这才是一个典型的01背包的问题:
\[f(i, j) = f(i - 1, j) + f(i - 1, j - nums[i])
\]
\(f(i, j)\)表示前 i 个物品装入空间为 j 的背包可能的数目。
python 代码如下,这里需要注意的是,在初始化 dp 数组时,dp[0][0]
为 1, 表示将 0 个物品放入 0 的背包的可能的次数为 1,并且其他行的第 0 个都是类似的情况。但下面就需要注意了,在传统的01背包中,第二个循环是从 1 开始的,但是这里因为可能的存在数字 0,所以我们必须从 0 开始。
from typing import List
class Solution:
def findTargetSumWays(self, nums: List[int], S: int) -> int:
N = len(nums)
nums = [0, ] + nums
# 判断可能性
p, r = divmod(sum(nums) + S, 2)
if r != 0 or abs(S) > sum(nums):
return 0
# dp 数组
dp = [[1] + [0] * p for _ in range(N + 1)]
for i in range(1, N + 1):
for j in range(p + 1):
if j - nums[i] >= 0:
dp[i][j] = dp[i-1][j] + dp[i-1][j - nums[i]]
else:
dp[i][j] = dp[i-1][j]
return dp[-1][-1]
当然也可以进行空间的优化,注意第二个循环时反向的:
from typing import List
class Solution:
def findTargetSumWays(self, nums: List[int], S: int) -> int:
N = len(nums)
nums = [0, ] + nums
# 判断可能性
p, r = divmod(sum(nums) + S, 2)
if r != 0 or abs(S) > sum(nums):
return 0
# dp 数组
dp = [1] + [0] * p
for i in range(1, N + 1):
for j in range(p, 0-1, -1):
if j - nums[i] >= 0:
dp[j] = dp[j] + dp[j - nums[i]]
return dp[-1]
总结
- 观察题目,这样的题目可能能够推导出更加简单的公式。
- 注意01背包的初始化以及循环,针对不同的题目,可能会存在不同的情况。