494. 目标和
文章目录
- [494. 目标和](https://leetcode.cn/problems/target-sum/)
- 一、题目
- 二、题解
- 方法一:目标和路径计数算法
- 方法二:01背包
- 方法三:01背包一维数组
一、题目
给你一个非负整数数组 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
二、题解
方法一:目标和路径计数算法
思考过程可以分为以下几步:
-
问题拆解: 首先,将问题拆解为更小的子问题。在这个问题中,我们可以将目标和问题拆解为在给定数组中选择一些数字,使得它们的和等于目标值。
-
状态定义: 确定动态规划的状态。在这个问题中,我们可以考虑使用二维数组
dp[i][j]
表示在前i
个数字中,和为j
的表达式的数量。 -
状态转移方程: 找到状态之间的转移关系。这是动态规划问题的核心。在这个问题中,对于每个数字
nums[i]
,我们有两种选择:添加正号或者添加负号。因此,状态转移方程可以表示为:dp[i][j] = dp[i-1][j-nums[i]] + dp[i-1][j+nums[i]]
,即前i-1
个数字和为j-nums[i]
的表达式数量加上前i-1
个数字和为j+nums[i]
的表达式数量,最后的数字是dp[i][j] = dp[i-1][abs(j-nums[i])] + dp[i-1][j+nums[i]]
,因为如果j < nums[i]
,我们是可以通过dp[i-1][nums[i]-j]
得到dp[i][j]
的。举个例子,如图,为什么dp[2][0] = 2
?图中已经说明白了。
-
边界条件: 根据问题的实际情况,确定边界条件。在这个问题中,我们可以从第一个数字开始考虑,初始化
dp[0][j]
,表示只有一个数字时,和为j
的表达式数量。 -
问题求解: 根据状态转移方程和边界条件,从小问题逐步求解到原问题。填充二维数组
dp
,最终dp[nums.size()-1][abs(target)]
就是我们要的答案,表示在所有数字中,和为target
的表达式数量。
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
// 定义一个二维数组 dp,大小为 (nums.size(), 2001),其中 dp[i][j] 表示在前 i 个数字中,和为 j 的表达式数量
vector<vector<int>> dp(nums.size(), vector<int>(2001, 0));
// 初始化:处理只有一个数字的情况
for(int i = 0; i <= 1000; i++){
if(nums[0] == i){
if(nums[0] == 0) dp[0][i] = 2; // 对于数字 0,可以有正号或负号
else dp[0][i] = 1;
}
}
// 填写 dp 数组
for(int i = 1; i < nums.size(); i++){
for(int j = 0; j <= 1000; j++){
dp[i][j] = dp[i-1][abs(j-nums[i])] + dp[i-1][j+nums[i]]; // 根据状态转移方程计算 dp[i][j]
}
}
return dp[nums.size()-1][abs(target)]; // 返回在所有数字中,和为 target 的表达式数量
}
};
方法二:01背包
01背包问题通常是这样的:给定一组物品,每个物品有重量和价值,我们要选择一些物品放入一个容量有限的背包中,使得背包中物品的总重量不超过背包的容量,同时最大化物品的总价值。
在这道题中,我们可以将正数部分和负数部分(前面是加号的和前面是减号的)的数字分别看作两组物品。正数部分的数字相当于具有正的价值,负数部分的数字相当于具有负的价值。
具体步骤如下:
-
将数组
nums
拆分成两个子数组:add
和minus
。add
包含所有正数部分的数字,minus
包含所有负数部分的数字。我们可以得到
add-minus = target
,add+minus = sum
,从而根据这两个式子得出(sum+target)/2 = add
。 -
使用01背包的思想:这个问题就转化成了所有数字凑成
add
这个数的方法。即背包容量是add
,物品是nums数组里的所有元素。 -
使用动态规划来解决问题:我们创建一个二维数组
dp
,其中dp[i][j]
表示在考虑前i
个物品时,总和等于j
的方法数。- 初始化
dp[0][0]
为 1,因为当没有物品可选时,总和为 0 是唯一的一种方式;但如果nums[0] = 0,还有一种方式就是只取nums[0]
,则有两种方式。 - 对于其它物品
nums[i]
,我们根据以下规则更新dp[i][j]
:- 如果
j >= nums[i]
,那么我们可以选择是否将nums[i]
放入背包,dp[i][j]
等于不放入背包和放入背包方式的总和,即dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]]
。 - 如果
j < nums[i]
,则nums[i]
不可以放入背包dp[i][j] = dp[i-1][j]
。
- 如果
- 初始化
-
最终的答案是
dp[nums.size()-1][add]
,表示在考虑所有物品后,总和等于add
的方法数。
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = 0;
// 计算数组nums的总和
for(int i = 0; i < nums.size(); i++){
sum += nums[i];
}
// 如果总和与目标值之和为奇数,或者总和小于目标值的绝对值,则无解,返回0
if((sum + target) % 2 == 1 || sum < abs(target)){
return 0;
}
// 计算要构造的正数部分和,这是01背包问题中的背包容量
int add = (sum + target) / 2;
// 创建二维动态规划数组dp
vector<vector<int>> dp(nums.size(),vector<int>(add+1, 0));
// 初始化dp数组
for(int i = 0; i <= add; i++){
// 如果i等于第一个数字nums[0],表示可以用第一个数字构造和为i的方式有1种
if(i == nums[0]){
dp[0][i] = 1;
}
}
// 特殊情况处理
if(nums[0] == 0) dp[0][0] = 2;
// 普通情况:当没有物品可选时,总和为 0 是唯一的一种方式
else dp[0][0] = 1;
// 填写dp数组
for(int i = 1; i < nums.size(); i++){
for(int j = 0; j <= add; j++){
// 如果当前数字nums[i]大于目标和j,无法用当前数字构造
if(nums[i] > j) dp[i][j] = dp[i-1][j];
// 选择是否将 `nums[i]` 放入背包,`dp[i][j]`等于不放入背包和放入背包方式的总和
else dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]];
}
}
// 返回最终的结果,即在考虑所有数字后,构造和为add的方法数
return dp[nums.size()-1][add];
}
};
方法三:01背包一维数组
具体细节就是初始化这里,这个dp[0] = 1相当于是按照结论推初始化(因为按照二维的初始化方式是错误的),最好还是去理解一下二维的吧……
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int sum = 0;
for (int i = 0; i < nums.size(); i++) sum += nums[i];
if (abs(target) > sum) return 0;
if ((target + sum) % 2 == 1) return 0;
int add = (target + sum) / 2;
vector<int> dp(add + 1, 0);
dp[0] = 1;
for (int i = 0; i < nums.size(); i++) {
for (int j = add; j >= nums[i]; j--) {
dp[j] += dp[j - nums[i]];
}
}
return dp[add];
}
};