leetcode-master/problems/0494.目标和.md

1026 lines
33 KiB
Markdown
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

* [做项目多个C++、Java、Go、测开、前端项目](https://www.programmercarl.com/other/kstar.html)
* [刷算法(两个月高强度学算法)](https://www.programmercarl.com/xunlian/xunlianying.html)
* [背八股40天挑战高频面试题](https://www.programmercarl.com/xunlian/bagu.html)
# 494.目标和
[力扣题目链接](https://leetcode.cn/problems/target-sum/)
难度:中等
给定一个非负整数数组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 位整数存下。
## 算法公开课
**[《代码随想录》算法视频公开课](https://programmercarl.com/other/gongkaike.html)[装满背包有多少种方法?| LeetCode494.目标和](https://www.bilibili.com/video/BV1o8411j73x/),相信结合视频再看本篇题解,更有助于大家对本题的理解**。
## 思路
如果对背包问题不都熟悉先看这两篇:
* [动态规划关于01背包问题你该了解这些](https://programmercarl.com/背包理论基础01背包-1.html)
* [动态规划关于01背包问题你该了解这些滚动数组](https://programmercarl.com/背包理论基础01背包-2.html)
如果跟着「代码随想录」一起学过[回溯算法系列](https://programmercarl.com/回溯总结.html)的录友,看到这道题,应该有一种直觉,就是感觉好像回溯法可以暴搜出来。
事实确实如此,下面我也会给出相应的代码,只不过会超时。
这道题目咋眼一看和动态规划背包啥的也没啥关系。
本题要如何使表达式结果为target
既然为target那么就一定有 left组合 - right组合 = target。
left + right = sum而sum是固定的。right = sum - left
left - (sum - left) = target 推导出 left = (target + sum)/2 。
target是固定的sum是固定的left就可以求出来。
此时问题就是在集合nums中找出和为left的组合。
### 回溯算法
在回溯算法系列中,一起学过这道题目[回溯算法39. 组合总和](https://programmercarl.com/0039.组合总和.html)的录友应该感觉很熟悉,这不就是组合总和问题么?
此时可以套组合总和的回溯法代码,几乎不用改动。
当然,也可以转变成序列区间选+ 或者 -,使用回溯法,那就是另一个解法。
我也把代码给出来吧,大家可以了解一下,回溯的解法,以下是本题转变为组合总和问题的回溯法代码:
```CPP
class Solution {
private:
vector<vector<int>> result;
vector<int> path;
void backtracking(vector<int>& candidates, int target, int sum, int startIndex) {
if (sum == target) {
result.push_back(path);
}
// 如果 sum + candidates[i] > target 就终止遍历
for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
sum += candidates[i];
path.push_back(candidates[i]);
backtracking(candidates, target, sum, i + 1);
sum -= candidates[i];
path.pop_back();
}
}
public:
int findTargetSumWays(vector<int>& nums, int S) {
int sum = 0;
for (int i = 0; i < nums.size(); i++) sum += nums[i];
if (S > sum) return 0; // 此时没有方案
if ((S + sum) % 2) return 0; // 此时没有方案两个int相加的时候要格外小心数值溢出的问题
int bagSize = (S + sum) / 2; // 转变为组合总和问题bagsize就是要求的和
// 以下为回溯法代码
result.clear();
path.clear();
sort(nums.begin(), nums.end()); // 需要排序
backtracking(nums, bagSize, 0, 0);
return result.size();
}
};
```
当然以上代码超时了。
也可以使用记忆化回溯,但这里我就不在回溯上下功夫了,直接看动规吧
### 动态规划 二维dp数组
假设加法的总和为x那么减法对应的总和就是sum - x。
所以我们要求的是 x - (sum - x) = target
x = (target + sum) / 2
**此时问题就转化为用nums装满容量为x的背包有几种方法**
这里的x就是bagSize也就是我们后面要求的背包容量。
大家看到`(target + sum) / 2` 应该担心计算的过程中向下取整有没有影响。
这么担心就对了例如sum是5target是2 的话其实就是无解的,所以:
```CPP
C++代码中输入的S 就是题目描述的 target
if ((target + sum) % 2 == 1) return 0; // 此时没有方案
```
同时如果target 的绝对值已经大于sum那么也是没有方案的。
```CPP
if (abs(target) > sum) return 0; // 此时没有方案
```
因为每个物品题目中的1只用一次
这次和之前遇到的背包问题不一样了之前都是求容量为j的背包最多能装多少。
本题则是装满有几种方法。其实这就是一个组合问题了。
#### 1. 确定dp数组以及下标的含义
先用 二维 dp数组求解本题dp[i][j]:使用 下标为[0, i]的nums[i]能够凑满j包括j这么大容量的包有dp[i][j]种方法。
01背包为什么这么定义dp数组我在[0-1背包理论基础](https://www.programmercarl.com/%E8%83%8C%E5%8C%85%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%8001%E8%83%8C%E5%8C%85-1.html)中 确定dp数组的含义里讲解过。
#### 2. 确定递推公式
我们先手动推导一下,这个二维数组里面的数值。
------------
先只考虑物品0如图
![](https://file1.kamacoder.com/i/algo/20240808161747.png)
这里的所有物品都是题目中的数字1
装满背包容量为0 的方法个数是1即 放0件物品。
装满背包容量为1 的方法个数是1即 放物品0。
装满背包容量为2 的方法个数是0目前没有办法能装满容量为2的背包。
--------------
接下来 考虑 物品0 和 物品1如图
![](https://file1.kamacoder.com/i/algo/20240808162052.png)
装满背包容量为0 的方法个数是1即 放0件物品。
装满背包容量为1 的方法个数是2即 放物品0 或者 放物品1。
装满背包容量为2 的方法个数是1即 放物品0 和 放物品1。
其他容量都不能装满所以方法是0。
-----------------
接下来 考虑 物品0 、物品1 和 物品2 ,如图:
![](https://file1.kamacoder.com/i/algo/20240808162533.png)
装满背包容量为0 的方法个数是1即 放0件物品。
装满背包容量为1 的方法个数是3即 放物品0 或者 放物品1 或者 放物品2。
装满背包容量为2 的方法个数是3即 放物品0 和 放物品1、放物品0 和 物品2、放物品1 和 物品2。
装满背包容量为3的方法个数是1即 放物品0 和 物品1 和 物品2。
---------------
通过以上举例,我们来看 dp[2][2] 可以有哪些方向推出来。
如图红色部分:
![](https://file1.kamacoder.com/i/algo/20240808163312.png)
dp[2][2] = 3即 放物品0 和 放物品1、放物品0 和 物品 2、放物品1 和 物品2 如图所示,三种方法:
![](https://file1.kamacoder.com/i/algo/20240826111946.png)
**容量为2 的背包,如果不放 物品2 有几种方法呢**
有 dp[1][2] 种方法,即 背包容量为2只考虑物品0 和 物品1 ,有 dp[1][2] 种方法,如图:
![](https://file1.kamacoder.com/i/algo/20240826112805.png)
**容量为2 的背包, 如果放 物品2 有几种方法呢**
首先 要在背包里 先把物品2的容量空出来 装满 刨除物品2容量 的背包 有几种方法呢?
刨除物品2容量后的背包容量为 1。
此时装满背包容量为1 有 dp[1][1] 种方法,即: 不放物品2背包容量为1只考虑物品 0 和 物品 1有 dp[1][1] 种方法。
如图:
![](https://file1.kamacoder.com/i/algo/20240826113043.png)
有录友可能疑惑,这里计算的是放满 容量为2的背包 有几种方法那物品2去哪了
在上面图中你把物品2补上就好同样是两种方法。
dp[2][2] = 容量为2的背包不放物品2有几种方法 + 容量为2的背包放物品2有几种方法
所以 dp[2][2] = dp[1][2] + dp[1][1] ,如图:
![](https://file1.kamacoder.com/i/algo/20240826113258.png)
以上过程,抽象化如下:
* **不放物品i**即背包容量为j里面不放物品i装满有dp[i - 1][j]中方法。
* **放物品i**先空出物品i的容量背包容量为j - 物品i容量放满背包有 dp[i - 1][j - 物品i容量] 种方法。
本题中物品i的容量是nums[i]价值也是nums[i]。
递推公式dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]];
考到这个递推公式,我们应该注意到,`j - nums[i]` 作为数组下标,如果 `j - nums[i]` 小于零呢?
说明背包容量装不下 物品i所以此时装满背包的方法值 等于 不放物品i的装满背包的方法dp[i][j] = dp[i - 1][j];
所以递推公式:
```CPP
if (nums[i] > j) dp[i][j] = dp[i - 1][j];
else dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]];
```
#### 3. dp数组如何初始化
先明确递推的方向,如图,求解 dp[2][2] 是由 上方和左上方推出。
![](https://file1.kamacoder.com/i/algo/20240826115800.png)
那么二维数组的最上行 和 最左列一定要初始化,这是递推公式推导的基础,如图红色部分:
![](https://file1.kamacoder.com/i/algo/20240827103507.png)
关于dp[0][0]的值在上面的递推公式讲解中已经讲过装满背包容量为0 的方法数量是1即 放0件物品。
那么最上行dp[0][j] 如何初始化呢?
dp[0][j]只放物品0 把容量为j的背包填满有几种方法。
只有背包容量为 物品0 的容量的时候方法为1正好装满。
其他情况下,要不是装不满,要不是装不下。
所以初始化dp[0][nums[0]] = 1 其他均为0 。
表格最左列也要初始化dp[i][0] : 背包容量为0 放物品0 到 物品i装满有几种方法。
都是有一种方法就是放0件物品。
即 dp[i][0] = 1
但这里有例外,就是如果 物品数值就是0呢
如果有两个物品物品0为0 物品1为0装满背包容量为0的方法有几种。
* 放0件物品
* 放物品0
* 放物品1
* 放物品0 和 物品1
此时是有4种方法。
其实就是算数组里有t个0然后按照组合数量求即 2^t 。
初始化如下:
```CPP
int numZero = 0;
for (int i = 0; i < nums.size(); i++) {
if (nums[i] == 0) numZero++;
dp[i][0] = (int) pow(2.0, numZero);
}
```
#### 4. 确定遍历顺序
在明确递推方向时,我们知道 当前值 是由上方和左上方推出。
那么我们的遍历顺序一定是 从上到下,从左到右。
因为只有这样,我们才能基于之前的数值做推导。
例如下图,如果上方没数值,左上方没数值,就无法推出 dp[2][2]。
![](https://file1.kamacoder.com/i/algo/20240827105427.png)
那么是先 从上到下 ,再从左到右遍历,例如这样:
```CPP
for (int i = 1; i < nums.size(); i++) { // 遍历物品
for (int j = 0; j <= bagSize; j++) { // 遍历背包
}
}
```
还是先 从左到右再从上到下呢例如这样
```CPP
for (int j = 0; j <= bagSize; j++) { // 遍历背包
for (int i = 1; i < nums.size(); i++) { // 遍历物品
}
}
```
**其实以上两种遍历都可以** 但仅针对二维DP数组是这样的
这一点我在 [01背包理论基础](https://www.programmercarl.com/%E8%83%8C%E5%8C%85%E7%90%86%E8%AE%BA%E5%9F%BA%E7%A1%8001%E8%83%8C%E5%8C%85-1.html)中的 遍历顺序部分讲过
这里我再画图讲一下以求dp[2][2]为例当先从上到下再从左到右遍历矩阵是这样
![](https://file1.kamacoder.com/i/algo/20240827110933.png)
当先从左到右再从上到下遍历矩阵是这样
![](https://file1.kamacoder.com/i/algo/20240827111013.png)
这里大家可以看出无论是以上哪种遍历都不影响 dp[2][2]的求值用来 推导 dp[2][2] 的数值都在
#### 5. 举例推导dp数组
输入nums: [1, 1, 1, 1, 1], target: 3
bagSize = (target + sum) / 2 = (3 + 5) / 2 = 4
dp数组状态变化如下
![](https://file1.kamacoder.com/i/algo/20240827111612.png)
这么大的矩阵我们是可以自己手动模拟出来的
在模拟的过程中既可以帮我们寻找规律也可以帮我们验证 递推公式加遍历顺序是不是按照我们想象的结果推进的
最后二维dp数组的C++代码如下
```CPP
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 bagSize = (target + sum) / 2;
vector<vector<int>> dp(nums.size(), vector<int>(bagSize + 1, 0));
// 初始化最上行
if (nums[0] <= bagSize) dp[0][nums[0]] = 1;
// 初始化最左列,最左列其他数值在递推公式中就完成了赋值
dp[0][0] = 1;
int numZero = 0;
for (int i = 0; i < nums.size(); i++) {
if (nums[i] == 0) numZero++;
dp[i][0] = (int) pow(2.0, numZero);
}
// 以下遍历顺序行列可以颠倒
for (int i = 1; i < nums.size(); i++) { // 遍历物品
for (int j = 0; j <= bagSize; j++) { // 遍历背包
if (nums[i] > j) dp[i][j] = dp[i - 1][j];
else dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]];
}
}
return dp[nums.size() - 1][bagSize];
}
};
```
### 动态规划 一维dp数组
将二维dp数组压缩成一维dp数组我们在 [01背包理论基础滚动数组](https://programmercarl.com/背包理论基础01背包-2.html) 讲过滚动数组,原理是一样的,即重复利用每一行的数值。
既然是重复利用每一行,就是将二维数组压缩成一行。
dp[i][j] 去掉 行的维度,即 dp[j]表示填满j包括j这么大容积的包有dp[j]种方法。
#### 2. 确定递推公式
二维DP数组递推公式 `dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]];`
去掉维度i 之后,递推公式:`dp[j] = dp[j] + dp[j - nums[i]]` ,即:`dp[j] += dp[j - nums[i]]`
**这个公式在后面在讲解背包解决排列组合问题的时候还会用到!**
#### 3. dp数组如何初始化
在上面 二维dp数组中我们讲解过 dp[0][0] 初始为1这里dp[0] 同样初始为1 ,即装满背包为0的方法有一种放0件物品。
#### 4. 确定遍历顺序
在[动态规划关于01背包问题你该了解这些滚动数组](https://programmercarl.com/背包理论基础01背包-2.html)中我们系统讲过对于01背包问题一维dp的遍历。
遍历物品放在外循环,遍历背包在内循环,且内循环倒序(为了保证物品只使用一次)。
#### 5. 举例推导dp数组
输入nums: [1, 1, 1, 1, 1], target: 3
bagSize = (target + sum) / 2 = (3 + 5) / 2 = 4
dp数组状态变化如下
![](https://file1.kamacoder.com/i/algo/20210125120743274.jpg)
大家可以和 二维dp数组的打印结果做一下对比。
一维DP的C++代码如下:
```CPP
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 bagSize = (target + sum) / 2;
vector<int> dp(bagSize + 1, 0);
dp[0] = 1;
for (int i = 0; i < nums.size(); i++) {
for (int j = bagSize; j >= nums[i]; j--) {
dp[j] += dp[j - nums[i]];
}
}
return dp[bagSize];
}
};
```
* 时间复杂度O(n × m)n为正数个数m为背包容量
* 空间复杂度O(m)m为背包容量
### 拓展
关于一维dp数组的递推公式解释也可以从以下维度来理解。 **但还是从二维DP数组到一维DP数组这样更容易理解一些**
2. 确定递推公式
有哪些来源可以推出dp[j]呢?
只要搞到nums[i]凑成dp[j]就有dp[j - nums[i]] 种方法。
例如dp[j]j 为5
* 已经有一个1nums[i] 的话,有 dp[4]种方法 凑成 容量为5的背包。
* 已经有一个2nums[i] 的话,有 dp[3]种方法 凑成 容量为5的背包。
* 已经有一个3nums[i] 的话,有 dp[2]种方法 凑成 容量为5的背包
* 已经有一个4nums[i] 的话,有 dp[1]种方法 凑成 容量为5的背包
* 已经有一个5 nums[i])的话,有 dp[0]种方法 凑成 容量为5的背包
那么凑整dp[5]有多少方法呢,也就是把 所有的 dp[j - nums[i]] 累加起来。
所以求组合类问题的公式,都是类似这种:
```
dp[j] += dp[j - nums[i]]
```
## 总结
此时 大家应该不禁想起,我们之前讲过的[回溯算法39. 组合总和](https://programmercarl.com/0039.组合总和.html)是不是应该也可以用dp来做啊
是可以求的如果仅仅是求个数的话就可以用dp但[回溯算法39. 组合总和](https://programmercarl.com/0039.组合总和.html)要求的是把所有组合列出来,还是要使用回溯法暴搜的。
本题还是有点难度理解上从二维DP数组更容易理解做题上直接用一维DP更简洁一些。
大家可以选择哪种方式自己更容易理解。
在后面得题目中,在求装满背包有几种方法的情况下,递推公式一般为:
```CPP
dp[j] += dp[j - nums[i]];
```
我们在讲解完全背包的时候,还会用到这个递推公式!
## 其他语言版本
### Java
```java
class Solution {
public int findTargetSumWays(int[] nums, int target) {
int sum = 0;
for (int i = 0; i < nums.length; i++) sum += nums[i];
//如果target的绝对值大于sum那么是没有方案的
if (Math.abs(target) > sum) return 0;
//如果(target+sum)除以2的余数不为0也是没有方案的
if ((target + sum) % 2 == 1) return 0;
int bagSize = (target + sum) / 2;
int[] dp = new int[bagSize + 1];
dp[0] = 1;
for (int i = 0; i < nums.length; i++) {
for (int j = bagSize; j >= nums[i]; j--) {
dp[j] += dp[j - nums[i]];
}
}
return dp[bagSize];
}
}
```
易于理解的二维数组版本:
```java
class Solution {
public int findTargetSumWays(int[] nums, int target) {
// 01背包应用之“有多少种不同的填满背包最大容量的方法“
// 易于理解的二维数组解法及详细注释
int sum = 0;
for(int i = 0; i < nums.length; i++) {
sum += nums[i];
}
// 注意nums[i] >= 0的题目条件意味着sum也是所有nums[i]的绝对值之和
// 这里保证了sum + target一定是大于等于零的也就是left大于等于零毕竟我们定义left大于right
if(sum < Math.abs(target)){
return 0;
}
// 利用二元一次方程组将left用target和sum表示出来替换掉right组合详见代码随想录对此题的分析
// 如果所求的left数组和为小数则作为整数数组的nums里的任何元素自然是没有办法凑出这个小数的
if((sum + target) % 2 != 0) {
return 0;
}
int left = (sum + target) / 2;
// dp[i][j]遍历到数组第i个数时 left为j时的能装满背包的方法总数
int[][] dp = new int[nums.length][left + 1];
// 初始化最上行dp[0][j])当nums[0] == j时注意nums[0]和j都一定是大于等于零的因此不需要判断等于-j时的情况有唯一一种取法可取到jdp[0][j]此时等于1
// 其他情况dp[0][j] = 0
// java整数数组默认初始值为0
if (nums[0] <= left) {
dp[0][nums[0]] = 1;
}
// 初始化最左列dp[i][0])
// 当从nums数组的索引0到i的部分有n个0时n > 0)每个0可以取+/-因此有2的n次方中可以取到j = 0的方案
// n = 0说明当前遍历到的数组部分没有0全为正数因此只有一种方案可以取到j = 0就是所有数都不取
int numZeros = 0;
for(int i = 0; i < nums.length; i++) {
if(nums[i] == 0) {
numZeros++;
}
dp[i][0] = (int) Math.pow(2, numZeros);
}
// 递推公式分析:
// 当nums[i] > j时这时候nums[i]一定不能取所以是dp[i - 1][j]种方案数
// nums[i] <= j时num[i]可取可不取因此方案数是dp[i - 1][j] + dp[i - 1][j - nums[i]]
// 由递推公式可知先遍历i或j都可
for(int i = 1; i < nums.length; i++) {
for(int j = 1; j <= left; j++) {
if(nums[i] > j) {
dp[i][j] = dp[i - 1][j];
} else {
dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]];
}
}
}
return dp[nums.length - 1][left];
}
}
```
### Python
回溯版
```python
class Solution:
def backtracking(self, candidates, target, total, startIndex, path, result):
if total == target:
result.append(path[:]) # 将当前路径的副本添加到结果中
# 如果 sum + candidates[i] > target则停止遍历
for i in range(startIndex, len(candidates)):
if total + candidates[i] > target:
break
total += candidates[i]
path.append(candidates[i])
self.backtracking(candidates, target, total, i + 1, path, result)
total -= candidates[i]
path.pop()
def findTargetSumWays(self, nums: List[int], target: int) -> int:
total = sum(nums)
if target > total:
return 0 # 此时没有方案
if (target + total) % 2 != 0:
return 0 # 此时没有方案,两个整数相加时要注意数值溢出的问题
bagSize = (target + total) // 2 # 转化为组合总和问题bagSize就是目标和
# 以下是回溯法代码
result = []
nums.sort() # 需要对nums进行排序
self.backtracking(nums, bagSize, 0, 0, [], result)
return len(result)
```
二维DP
```python
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
total_sum = sum(nums) # 计算nums的总和
if abs(target) > total_sum:
return 0 # 此时没有方案
if (target + total_sum) % 2 == 1:
return 0 # 此时没有方案
target_sum = (target + total_sum) // 2 # 目标和
# 创建二维动态规划数组,行表示选取的元素数量,列表示累加和
dp = [[0] * (target_sum + 1) for _ in range(len(nums) + 1)]
dp = [[0] * (target_sum + 1) for _ in range(len(nums))]
# 初始化状态
dp[0][0] = 1
if nums[0] <= target_sum:
dp[0][nums[0]] = 1
numZero = 0
for i in range(len(nums)):
if nums[i] == 0:
numZero += 1
dp[i][0] = int(math.pow(2, numZero))
# 动态规划过程
for i in range(1, len(nums)):
for j in range(target_sum + 1):
dp[i][j] = dp[i - 1][j] # 不选取当前元素
if j >= nums[i]: #只有j >= 本次遍历元素才可以选取当前元素
dp[i][j] += dp[i - 1][j - nums[i]] # 选取当前元素
return dp[len(nums)-1][target_sum] # 返回达到目标和的方案数
```
一维DP
```python
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
total_sum = sum(nums) # 计算nums的总和
if abs(target) > total_sum:
return 0 # 此时没有方案
if (target + total_sum) % 2 == 1:
return 0 # 此时没有方案
target_sum = (target + total_sum) // 2 # 目标和
dp = [0] * (target_sum + 1) # 创建动态规划数组初始化为0
dp[0] = 1 # 当目标和为0时只有一种方案即什么都不选
for num in nums:
for j in range(target_sum, num - 1, -1):
dp[j] += dp[j - num] # 状态转移方程,累加不同选择方式的数量
return dp[target_sum] # 返回达到目标和的方案数
```
### Go
回溯法思路
```go
func findTargetSumWays(nums []int, target int) int {
var result int
var backtracking func(nums []int, target int, index int, currentSum int)
backtracking = func(nums []int, target int, index int, currentSum int) {
if index == len(nums) {
if currentSum == target {
result++
}
return
}
// 选择加上当前数字
backtracking(nums, target, index+1, currentSum+nums[index])
// 选择减去当前数字
backtracking(nums, target, index+1, currentSum-nums[index])
}
backtracking(nums, target, 0, 0)
return result
}
```
二维dp
```go
func findTargetSumWays(nums []int, target int) int {
sum := 0
for _, v := range nums {
sum += v
}
if math.Abs(float64(target)) > float64(sum) {
return 0 // 此时没有方案
}
if (target + sum) % 2 == 1 {
return 0 // 此时没有方案
}
bagSize := (target + sum) / 2
dp := make([][]int, len(nums))
for i := range dp {
dp[i] = make([]int, bagSize + 1)
}
// 初始化最上行
if nums[0] <= bagSize {
dp[0][nums[0]] = 1
}
// 初始化最左列,最左列其他数值在递推公式中就完成了赋值
dp[0][0] = 1
var numZero float64
for i := range nums {
if nums[i] == 0 {
numZero++
}
dp[i][0] = int(math.Pow(2, numZero))
}
// 以下遍历顺序行列可以颠倒
for i := 1; i < len(nums); i++ { // 行,遍历物品
for j := 0; j <= bagSize; j++ { // 列,遍历背包
if nums[i] > j {
dp[i][j] = dp[i-1][j]
} else {
dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]]
}
}
}
return dp[len(nums)-1][bagSize]
}
```
一维dp
```go
func findTargetSumWays(nums []int, target int) int {
sum := 0
for _, v := range nums {
sum += v
}
if abs(target) > sum {
return 0
}
if (sum+target)%2 == 1 {
return 0
}
// 计算背包大小
bag := (sum + target) / 2
// 定义dp数组
dp := make([]int, bag+1)
// 初始化
dp[0] = 1
// 遍历顺序
for i := 0; i < len(nums); i++ {
for j := bag; j >= nums[i]; j-- {
//推导公式
dp[j] += dp[j-nums[i]]
//fmt.Println(dp)
}
}
return dp[bag]
}
func abs(x int) int {
return int(math.Abs(float64(x)))
}
```
### JavaScript
```javascript
/**
* 题目来源: {@link https://leetcode.cn/problems/target-sum/}
*
* 题解来源: {@link https://programmercarl.com/0494.%E7%9B%AE%E6%A0%87%E5%92%8C.html#%E7%AE%97%E6%B3%95%E5%85%AC%E5%BC%80%E8%AF%BE}
*
* 时间复杂度: O(n * C), C 为数组元素总和与目标值之和的一半
*
* 空间复杂度: O(C)
*
* @param { number[] } nums
* @param { number } target
* @return { number }
*/
const findTargetSumWays = (nums, target) => {
// 原题目可转化为:
//
// 将所有元素划分为 2 个集合,
// 一个集合中包含所有要添加 "+" 号的元素, 一个集合中包含所有要添加 "-" 号的元素
//
// 设两个集合的元素和分别为 positive 和 negative, 所有元素总和为 sum, 那么有如下等式:
// positive + negative = sum (1)
// positive - negative = target (2)
// (1) 与 (2) 联立可得: positive = (sum + target) / 2,
// 所以如果能从原数组中取出若干个元素形成 1 个元素总和为 (sum + target) / 2 的集合,
// 就算得到了 1 种满足题意的组合方法
//
// 因此, 所求变为: 有多少种取法, 可使得容量为 (sum + target) / 2 的背包被装满?
const sum = nums.reduce((a, b) => a + b);
if (Math.abs(target) > sum) {
return 0;
}
if ((target + sum) % 2) {
return 0;
}
const bagWeight = (target + sum) / 2;
// 1. dp 数组的含义
// dp[j]: 装满容量为 j 的背包, 有 dp[j] 种方法
let dp = new Array(bagWeight + 1).fill(0);
// 2. 递推公式
// dp[j] = Σ(dp[j - nums[j]]), (j ∈ [0, j] 且 j >= nums[j])
// 因为 dp[j - nums[j]] 表示: 装满容量为 j - nums[j] 背包有 dp[j - nums[j]] 种方法
// 而容量为 j - nums[j] 的背包只需要再将 nums[j] 放入背包就能使得背包容量达到 j
// 因此, 让背包容量达到 j 有 Σ(dp[j - nums[j]]) 种方法
// 3. dp 数组如何初始化
// dp[0] = 1, dp[1 ~ bagWeight] = 0
dp[0] = 1;
// 4. 遍历顺序
// 先物品后背包, 物品从前往后遍历, 背包容量从后往前遍历
for (let i = 0; i < nums.length; i++) {
for (let j = bagWeight; j >= nums[i]; j--) {
dp[j] += dp[j - nums[i]];
}
}
return dp[bagWeight];
};
```
### TypeScript
```ts
function findTargetSumWays(nums: number[], target: number): number {
// 把数组分成两个组合left, right.left + right = sum, left - right = target.
const sum: number = nums.reduce((a: number, b: number): number => a + b);
if ((sum + target) % 2 || Math.abs(target) > sum) return 0;
const left: number = (sum + target) / 2;
// 将问题转化为装满容量为left的背包有多少种方法
// dp[i]表示装满容量为i的背包有多少种方法
const dp: number[] = new Array(left + 1).fill(0);
dp[0] = 1; // 装满容量为0的背包有1种方法什么也不装
for (let i: number = 0; i < nums.length; i++) {
for (let j: number = left; j >= nums[i]; j--) {
dp[j] += dp[j - nums[i]];
}
}
return dp[left];
};
```
### Scala
```scala
object Solution {
def findTargetSumWays(nums: Array[Int], target: Int): Int = {
var sum = nums.sum
if (math.abs(target) > sum) return 0 // 此时没有方案
if ((sum + target) % 2 == 1) return 0 // 此时没有方案
var bagSize = (sum + target) / 2
var dp = new Array[Int](bagSize + 1)
dp(0) = 1
for (i <- 0 until nums.length; j <- bagSize to nums(i) by -1) {
dp(j) += dp(j - nums(i))
}
dp(bagSize)
}
}
```
### Rust
```rust
impl Solution {
pub fn find_target_sum_ways(nums: Vec<i32>, target: i32) -> i32 {
let sum = nums.iter().sum::<i32>();
if target.abs() > sum {
return 0;
}
if (target + sum) % 2 == 1 {
return 0;
}
let size = (sum + target) as usize / 2;
let mut dp = vec![0; size + 1];
dp[0] = 1;
for n in nums {
for s in (n as usize..=size).rev() {
dp[s] += dp[s - n as usize];
}
}
dp[size]
}
}
```
### C
```c
int getSum(int * nums, int numsSize){
int sum = 0;
for(int i = 0; i < numsSize; i++){
sum += nums[i];
}
return sum;
}
int findTargetSumWays(int* nums, int numsSize, int target) {
int sum = getSum(nums, numsSize);
int diff = sum - target;
// 两种情况不满足
if(diff < 0 || diff % 2 != 0){
return 0;
}
int bagSize = diff / 2;
int dp[numsSize + 1][bagSize + 1];
dp[0][0] = 1;
for(int i = 1; i <= numsSize; i++){
int num = nums[i - 1];
for(int j = 0; j <= bagSize; j++){
dp[i][j] = dp[i - 1][j];
if(j >= num){
dp[i][j] += dp[i - 1][j - num];
}
}
}
return dp[numsSize][bagSize];
}
```
### C#
```csharp
public class Solution
{
public int FindTargetSumWays(int[] nums, int target)
{
int sum = 0;
foreach (int num in nums)
{
sum += num;
}
if (Math.Abs(target) > sum) return 0;
if ((sum + target) % 2 == 1) return 0;
int bagSize = (sum + target) / 2;
int[] dp = new int[bagSize + 1];
dp[0] = 1;
for (int i = 0; i < nums.Length; i++)
{
for (int j = bagSize; j >= nums[i]; j--)
{
dp[j] += dp[j - nums[i]];
}
}
return dp[bagSize];
}
}
```