494. 目标和
494. 目标和
题目描述
给你一个非负整数数组 nums 和一个整数 target。
现在你有两种选择,可以在数组中的每个整数前添加:
'+''-'
然后把所有整数连接起来,构造一个表达式。
返回可以通过上述方法构造的、运算结果等于 target 的 不同表达式的数目。
示例 1
text
输入:nums = [1,1,1,1,1], target = 3
输出:5
示例 2
text
输入:nums = [1], target = 1
输出:1
约束
1 <= nums.length <= 200 <= nums[i] <= 10000 <= sum(nums[i]) <= 1000-1000 <= target <= 1000
核心思路
这题表面上看是在枚举:
- 每个数前面加
+ - 或者加
-
但真正问的是:
- 一共有多少种方式能凑出
target
所以它本质上是一个:
- 计数型搜索 / 动态规划 问题
这题至少有两种常见做法:
- 解法一:DFS / 回溯枚举加减(推荐先理解)
- 解法二:转化成 0/1 背包计数(推荐掌握)
先理解:为什么能转成背包?
假设我们把数组分成两部分:
- 一部分前面加
+,它们的和记作P - 一部分前面加
-,它们的和记作N
那么题目要求:
text
P - N = target
同时所有数总和是:
text
P + N = sum(nums)
两式相加得到:
text
2P = target + sum(nums)
所以:
text
P = (target + sum(nums)) / 2
这就变成了:
- 从
nums里选一些数 - 让它们的和恰好等于
P - 问有多少种选法
这就是典型的 0/1 背包计数问题。
但要注意两个前提:
target + sum(nums)必须是偶数|target|不能大于sum(nums)
否则答案直接是 0。
解法一:DFS / 回溯枚举加减
这是最直观、最适合第一次理解的做法。
思路拆解
对于每个数字 nums[i],我们都有两种选择:
- 加上它
- 减去它
所以可以递归枚举:
text
第 i 个数选 + 还是选 -
递归到最后一个数之后:
- 如果当前和等于
target,说明找到一种方案 - 否则不是答案
一步一步写出算法
第一步:定义递归函数
ts
const dfs = (index: number, sum: number): number => {
...
}
含义:
- 当前处理到下标
index - 当前表达式和为
sum - 返回从这里继续往后能凑出多少种方案
第二步:递归终止条件
如果所有数都处理完了:
ts
if (index === nums.length) {
return sum === target ? 1 : 0
}
第三步:枚举当前数的两种选择
ts
return dfs(index + 1, sum + nums[index]) + dfs(index + 1, sum - nums[index])
代码实现(TypeScript)
ts
function findTargetSumWays(nums: number[], target: number): number {
const dfs = (index: number, sum: number): number => {
if (index === nums.length) {
return sum === target ? 1 : 0
}
return dfs(index + 1, sum + nums[index]) + dfs(index + 1, sum - nums[index])
}
return dfs(0, 0)
}
复杂度分析
- 时间复杂度:
O(2^n) - 空间复杂度:
O(n)
因为每个数都有两种选择。
这一解法的优缺点
优点:
- 最直观
- 很容易理解题目本质
缺点:
- 指数级复杂度
- 数据稍大就慢
解法二:转化成 0/1 背包计数
这是这题最经典、最推荐掌握的做法。
思路拆解
前面已经推导出:
text
P = (target + sum(nums)) / 2
于是问题转化为:
- 从数组中选若干个数
- 恰好装满容量为
P的背包 - 问有多少种选法
这是标准的 0/1 背包计数。
我们定义:
ts
dp[j]
表示:
- 凑出和为
j的方案数
初始时:
ts
dp[0] = 1
表示:
- 什么都不选,凑出和
0有 1 种方式
然后依次遍历每个数,用倒序更新背包。
一步一步写出算法
第一步:先算总和
ts
const sum = nums.reduce((total, num) => total + num, 0)
第二步:先判断无解情况
如果:
ts
Math.abs(target) > sum
说明目标太大,不可能凑出来。
或者:
ts
(sum + target) % 2 !== 0
说明 P 不是整数,也不可能。
直接返回 0。
第三步:计算背包容量
ts
const positive = (sum + target) / 2
第四步:定义 DP 数组
ts
const dp = new Array(positive + 1).fill(0)
dp[0] = 1
第五步:遍历每个数字做 0/1 背包计数
ts
for (const num of nums) {
for (let j = positive; j >= num; j--) {
dp[j] += dp[j - num]
}
}
为什么要倒序?
因为:
- 每个数只能用一次
- 这是 0/1 背包的标准写法
第六步:返回答案
ts
return dp[positive]
代码实现(TypeScript)
ts
function findTargetSumWays(nums: number[], target: number): number {
const sum = nums.reduce((total, num) => total + num, 0)
if (Math.abs(target) > sum || (sum + target) % 2 !== 0) {
return 0
}
const positive = (sum + target) / 2
const dp = new Array(positive + 1).fill(0)
dp[0] = 1
for (const num of nums) {
for (let j = positive; j >= num; j--) {
dp[j] += dp[j - num]
}
}
return dp[positive]
}
4 8 6 9 2 1 =30 10
=>dp[0]=1 dp[4]=1
=>dp[8]=1 dp[0]=1 dp[4]=1
=>dp[10]=1 dp[8]=1 dp[6]=1 dp[4]=1 dp[0]=1
=>dp[10]=1 dp[9]=1 dp[8]=1 dp[6]=1 dp[4]=1 dp[0]=1
=>dp[10]=2 dp[9]=1 dp[8]=2 dp[6]=2 dp[4]=1 dp[2]=1
=>dp[10]=3
复杂度分析
- 时间复杂度:
O(n * positive) - 空间复杂度:
O(positive)
由于题目里总和不大,这个做法非常适合。
这一解法的优缺点
优点:
- 复杂度优秀
- 是这题标准解法
- 非常适合面试
缺点:
- 难点在于数学转化
- 第一次看不太容易想到
两种解法对比
| 解法 | 时间复杂度 | 空间复杂度 | 特点 |
|---|---|---|---|
| DFS / 回溯 | O(2^n) | O(n) | 最直观,适合入门 |
| 0/1 背包计数 | O(n * positive) | O(positive) | 标准高效解法 |
如果你是第一次做这题,建议先理解 DFS,再重点掌握背包转化。
手动模拟一遍背包解法
以:
text
nums = [1,1,1,1,1]
target = 3
为例。
总和:
text
sum = 5
所以:
text
P = (5 + 3) / 2 = 4
问题变成:
- 从五个
1里选一些数 - 让它们和为
4 - 一共有多少种选法?
显然就是:
- 选 5 个
1中的任意 4 个 - 方案数是
5
所以答案是:
text
5
面试时最容易错的点
1. 这题不是普通求和,是“方案数”
所以 DP 里存的不是布尔值,也不是最大值,而是:
- 方案数
2. 背包转化前要先判无解
这两句很关键:
ts
Math.abs(target) > sum
(sum + target) % 2 !== 0
3. 0/1 背包一定要倒序
ts
for (let j = positive; j >= num; j--)
否则就会变成完全背包,逻辑错误。
4. 0 这个数字也要正常处理
如果数组里有 0,它会让方案数翻倍。
而上面的 DP 写法本身就能正确处理这一点。
一句话总结
这题的本质是:
给每个数加正负号,等价于把数组分成正集合和负集合,再转成 0/1 背包计数。
- 想先理解题意:用 DFS 枚举加减
- 想写标准高效解:用 0/1 背包计数
推荐你最后记住的标准写法
如果你准备刷题,建议优先记下面这个背包版本:
ts
function findTargetSumWays(nums: number[], target: number): number {
const sum = nums.reduce((total, num) => total + num, 0)
if (Math.abs(target) > sum || (sum + target) % 2 !== 0) {
return 0
}
const positive = (sum + target) / 2
const dp = new Array(positive + 1).fill(0)
dp[0] = 1
for (const num of nums) {
for (let j = positive; j >= num; j--) {
dp[j] += dp[j - num]
}
}
return dp[positive]
}
因为这个版本:
- 是这题公认的标准解法
- 复杂度优秀
- 能顺便练到“表达式题转背包”的经典思路