494. 目标和
题目描述
给你一个非负整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
示例 1:
输入:nums = [1,1,1,1,1], target = 3
输出:5
解释:一共有 5 种方法让最终目标和为 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
+1 + 1 + 1 + 1 - 1 = 3
示例 2:
输入:nums = [1], target = 1
输出:1
提示:
- 1 <= nums.length <= 20
- 0 <= nums[i] <= 1000
- 0 <= sum(nums[i]) <= 1000
- -1000 <= target <= 1000
动态规划
一维数组
这段代码是一个使用一维动态规划数组解决目标和问题的实现。代码优化了空间复杂度,仅使用一维数组来记录状态。以下是对该代码的逐行注释和说明:
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
// 计算nums数组的总和
int sum = 0;
for(int i = 0; i < nums.size(); i++) sum += nums[i];
// 如果目标target的绝对值大于总和,或者(target + sum)为奇数,则不可能实现目标
if(abs(target) > sum || (sum + target) % 2 == 1) return 0;
// 计算动态规划的新目标值left,即数组可以达到的一个子集和
int left = (sum + target) / 2;
// 初始化动态规划的数组,大小为left+1
// dp[j]表示能够组合出和为j的方法数
vector<int> dp(left + 1, 0);
// 初始状态,没有元素时只能组成和为0的一种方式
dp[0] = 1;
// 遍历nums数组中的每个数字,更新dp数组
for(int i = 0; i < nums.size(); i++) {
// 从大到小更新dp数组,避免重复使用nums[i]更新dp[j]
for(int j = left; j >= nums[i]; j--) {
// 更新dp[j],dp[j-nums[i]]表示新增nums[i]之前组成j-nums[i]的方法数
// 现在可以通过加上nums[i]来达到和为j
dp[j] += dp[j - nums[i]];
}
}
// 返回最终能组成和为left的方法数,即为原问题的解
return dp[left];
}
};
这段代码通过转换问题,将原问题简化为一个子集和问题:它首先通过检查目标值target
与数组nums
元素总和sum
的关系来确定问题是否有解。如果target
可通过添加正负号到nums
的元素来达成,那么(sum + target)
必须是偶数。接着,将问题转换为寻找数组子集,其和等于(sum + target) / 2
。
一维动态规划数组dp
用来记录达到各个和j
的方法数。dp[0]
初始化为1,因为和为0总是有一种方法(不选取任何数字)。随后,代码通过遍历nums
中的每个数字来逐步构建达到目标子集和的方法数。由于一个数字只能用一次,遍历dp
数组时需要逆向更新,防止同一个数字被多次使用。
最终,dp[left]
即表示原问题的答案,即通过添加正负号到nums
的元素来达成目标target
的方法数。
二维数组
这段代码是解决“目标和”问题的一种动态规划解法。问题的核心是在于数列中的每个数前添加"+“或”-",使得最终表达式的结果等于给定的target。代码的基本逻辑是利用动态规划(DP)找出达到特定和(这里是变形后的target)的方法数。下面是对代码的逐行解释:
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = 0;
// 首先遍历给定数组nums,计算所有元素的总和sum
for(int i = 0; i < nums.size(); i++)
sum += nums[i];
// 如果target的绝对值大于sum,或者(sum + target)是奇数,则无法通过添加"+"和"-"达到target,返回0
if(abs(target) > sum || ((sum + target) % 2) == 1) return 0;
// 计算需要达到的正数和left,这部分是将原问题转化为一个子集和问题
int left = (sum + target) / 2;
int n = nums.size();
// 初始化动态规划表,dp[i][j]表示用前i个数可以构造出和为j的方法数
vector<vector<int>> dp(n + 1, vector<int>(left + 1, 0));
// base case,没有数字时唯一的方法就是构造出和为0
dp[0][0] = 1;
// 动态规划填表过程
for(int i = 1; i <= n; i++) {
for(int j = 0; j <= left; j++) {
// 先继承前i-1个数构造出和为j的方法数
dp[i][j] = dp[i - 1][j];
// 如果当前和j大于等于当前数字nums[i-1],则当前数字可以被加入构造和
// 此时的方法数等于不加当前数字时的方法数加上加了当前数字后的方法数
if(j >= nums[i - 1])
dp[i][j] += dp[i - 1][j - nums[i - 1]];
}
}
// 返回使用所有数字可以构造出和为left的方法数
return dp[n][left];
}
};
这个DP解法的核心思想是将原问题转化为一个子集和问题(一个经典的动态规划问题)。通过计算所有数字总和sum
和目标值target
的关系,我们可以得知需要达到的新目标和left
。然后,通过构建一个二维DP表来记录达到每个可能和的方法数,最终我们得到的dp[n][left]
就是问题的解答。
在这个问题中,dp
数组是动态规划中的一种常见技巧,用于存储中间结果,以避免重复计算,并最终解决问题。dp
数组的含义和初始化都是解题的关键部分。
dp
数组的含义
在这个特定问题中,dp[i][j]
代表的是,使用nums
数组中的前i
个数,能够组成和为j
的不同表达式的数量。这里的“前i
个数”是指索引从0到i-1
的nums
数组中的元素。
注意:是前i个
初始化
dp[0][0] = 1
:这表示不使用任何数字,构成和为0只有一种方法,即没有任何操作。这是递归解决方案的基础案例,也是动态规划的初始条件。- 对于
dp[0][1]
、dp[0][2]
等,它们被初始化为0,因为没有使用任何数字时,不可能组成除0以外的任何和。这反映了在不考虑任何nums
元素的情况下,不可能通过添加正负号来得到非零的和。
为什么dp[0][1]
、dp[0][2]
是0
这些初始化为0的值表示在不使用nums
数组中的任何数时,无法达成任何非零的目标和。这是一个重要的初始条件,因为它确立了构建解决方案时的基线。简而言之,没有数字可以选择意味着无法实现任何非零和,因此这些情况的可能性数量是0。
如何理解left=(sum+target)/2
这个转换基于一个数学技巧,通过将问题转换为寻找和为特定值的子集的数量,来简化问题。原问题要求我们通过在数组nums
的每个元素前添加正号或负号,来找到所有可能达到目标和target
的方式。
如果我们设所有正号的数之和为P
,所有负号的数之和的绝对值为N
,那么有:
- 总和
sum = P + N
- 目标和
target = P - N
通过解这两个方程,我们可以得到P = (sum + target) / 2
。也就是说,问题转化为了在nums
数组中找到和为P
的子集的数量,这就是为什么我们计算left=(sum+target)/2
并以此为基础进行动态规划的原因。
因此,动态规划解决方案基于这种转换,使用dp
数组来逐步构建达到目标子集和的方法数量,最终解决问题。