494. 目标和
问题描述
给定一个非负整数数组 nums
和一个整数 target
,向数组中的每个整数前添加 '+'
或 '-'
,然后串联成表达式,求共有多少种组合方式使得表达式运算结果等于 target
。
示例:
输入: nums = [1,1,1,1,1], target = 3
输出: 5
解释: 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
算法思路
动态规划(0-1背包问题)
:
- 数学推导:
- 设正数子集和为
S1
,负数子集和为S2
(取绝对值)。 - 有
S1 - S2 = target
和S1 + S2 = sum
(sum
为nums
总和)。 - 两式相加得:
2S1 = target + sum
→S1 = (target + sum) / 2
。
- 设正数子集和为
- 目标转换:
- 问题转化为在
nums
中选取若干元素,使其和等于(target + sum) / 2
的方案数。
- 问题转化为在
- 边界条件:
- 若
target > sum
或(target + sum)
为奇数 → 无解,返回 0。 - 若
(target + sum) < 0
→ 取绝对值(实际由边界条件保证非负)。
- 若
动态规划:
- 状态定义:
dp[j]
表示和为j
的方案数。
- 状态转移:
- 遍历每个数
num
,倒序更新j
从S
到num
:
dp[j] = dp[j] + dp[j - num]
- 遍历每个数
- 初始化:
dp[0] = 1
(和为 0 的方案数为 1,不选任何数字)。
代码实现
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sum = 0;
// 计算数组总和
for (int num : nums) {
sum += num;
}
// 边界条件1:target绝对值大于sum,无解
if (Math.abs(target) > sum) {
return 0;
}
// 边界条件2:sum+target为奇数,无解
if ((sum + target) % 2 != 0) {
return 0;
}
// 计算目标子集和S
int S = (sum + target) / 2;
// 确保S非负(实际由边界条件保证)
if (S < 0) {
S = -S; // 实际不会发生,因Math.abs(target)<=sum且(sum+target)为偶数
}
// 初始化dp数组:dp[j]表示和为 j 的方案数
int[] dp = new int[S + 1];
dp[0] = 1; // 空集的和为0,方案数为1
// 遍历每个数字
for (int num : nums) {
// 从S到num倒序遍历,避免重复使用同一数字
for (int j = S; j >= num; j--) {
// 状态转移:选或不选当前数字
dp[j] += dp[j - num];
}
}
return dp[S];
}
}
算法分析
- 时间复杂度:O(n×S)
其中n
为数组长度,S
为目标子集和。 - 空间复杂度:O(S)
使用一维 DP 数组。
算法过程
输入:nums = [1,1,1,1,1], target = 3
- 计算总和:
sum = 5
- 计算目标子集和:
S = (3 + 5) / 2 = 4
- 初始化 DP 数组:
dp = [1,0,0,0,0]
(dp[0]=1
) - 遍历数字:
num=1
:更新j=4→1
dp[4] += dp[3]
→ 0
dp[3] += dp[2]
→ 0
dp[2] += dp[1]
→ 0
dp[1] += dp[0]=1
→dp[1]=1
→dp=[1,1,0,0,0]
num=1
:更新j=4→1
dp[4] += dp[3]=0
dp[3] += dp[2]=0
dp[2] += dp[1]=1
→dp[2]=1
dp[1] += dp[0]=1
→dp[1]=2
→dp=[1,2,1,0,0]
- 后续同理,最终
dp[4]=5
- 结果:5
测试用例
public static void main(String[] args) {
Solution solution = new Solution();
// 测试用例1: 标准示例
int[] nums1 = {1,1,1,1,1};
System.out.println("Test 1: " + solution.findTargetSumWays(nums1, 3)); // 5
// 测试用例2: 目标值过大
int[] nums2 = {1,2,3};
System.out.println("Test 2: " + solution.findTargetSumWays(nums2, 10)); // 0
// 测试用例3: 目标和为负数
int[] nums3 = {1,2,3};
System.out.println("Test 3: " + solution.findTargetSumWays(nums3, -2)); // 1(方案:-1+2-3=-2 → 1种)
// 测试用例4: 无解(奇偶性不匹配)
int[] nums4 = {1,2,4};
System.out.println("Test 4: " + solution.findTargetSumWays(nums4, 1)); // 1(方案:-1-2+4=1)
// 测试用例5: 空数组
int[] nums5 = {};
System.out.println("Test 5: " + solution.findTargetSumWays(nums5, 0)); // 1(空表达式)
// 测试用例6: 目标和为0
int[] nums6 = {1,1,1,1};
System.out.println("Test 6: " + solution.findTargetSumWays(nums6, 0)); // 6
}
关键点
- 问题转化:
- 将添加
+/-
问题转化为子集和问题(背包问题)。
- 将添加
- 边界条件:
(target + sum)
必须为非负偶数。
- 动态规划核心:
- 状态转移:
dp[j] += dp[j - num]
。 - 倒序遍历避免重复计数。
- 状态转移:
- 初始化:
dp[0] = 1
(和为 0 的方案数为 1)。
常见问题
- 为什么需要倒序遍历
j
?
防止同一数字被重复使用(每个数字只能选一次)。 - 如何处理
target
为负数?
数学推导中(target + sum)
可处理负数,但需保证其为非负偶数。 - 为什么
dp[0]=1
?
表示不选任何数字时,和为 0 的方案数为 1(空表达式)。