* [做项目(多个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):[装满背包有多少种方法?| LeetCode:494.目标和](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> result; vector path; void backtracking(vector& 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& 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是5,target是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& 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> dp(nums.size(), vector(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& 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 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, * 已经有一个1(nums[i]) 的话,有 dp[4]种方法 凑成 容量为5的背包。 * 已经有一个2(nums[i]) 的话,有 dp[3]种方法 凑成 容量为5的背包。 * 已经有一个3(nums[i]) 的话,有 dp[2]种方法 凑成 容量为5的背包 * 已经有一个4(nums[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时的情况),有唯一一种取法可取到j,dp[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, target: i32) -> i32 { let sum = nums.iter().sum::(); 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]; } } ```