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
- 首先数组中每个数可以分为两种情况,即加或减,同样的,对于这个数之后的数也是加或减两种操作,很容易看出这就类似一颗二叉树,让根节点为 0,以便数组从首位开始分叉,树的第 x+1 层结点保存数组遍历到第 x 位时的和即可。比如[1,2,3] ,根节点 0,子节点为 0-1 和 0+1,-1 同样根据 2 分叉出
-1+2
和-1-2
,1也同样分叉出1+2
和1-2
…,代码可能更易看懂,每次累加分叉出的两种可能性即可 -
int[] ns; int N,T; public int findTargetSumWays(int[] nums, int target) { ns=nums; N=ns.length; T=target; return find(0,0); } // cur:数组下标,count:到下标 cur 为止数组的和 public int find(int cur,int count){ // 加到下标为 N 说明之前的 ns[0]~ns[N-1] 已经加完了 // 该看看是不是我们要的 target 了 if(cur == N){ if(count==T){ return 1; }else{ return 0; } } return find(cur+1, count+ns[cur]) + find(cur+1, count-ns[cur]); }
- 由于可变参数只有 cur 和 count 两个,并且在 dfs 某一层过程会重复计算很多次上一层的结果,所以可以记忆化该 dfs。
-
int[] ns; int N,T; Map<String,Integer> cache = new HashMap<>(); public int findTargetSumWays(int[] nums, int target) { ns=nums; N=ns.length; T=target; return find(0,0); } public int find(int cur,int count){ String key = cur+"_"+count; if(cache.containsKey(key)){ return cache.get(key); } if(cur == N){ cache.put(key,count==T?1:0); return cache.get(key); } int result = find(cur+1, count+ns[cur]) + find(cur+1, count-ns[cur]); cache.put(key,result); return result; }
- 能以递归形式记忆化搜索,那么就能够动态规划,设 f(x,y) 为取数组前 x 位,和为 y 的情况有多少种。因为每个数只能为加或减,所以f(x,y) 要么由数组前 x-1 位的结果加上第 x 位而来,或者为第 x-1 位的结果减去第 x 位而来,即
dp[i][j] = dp[i-1][j-nums[i-1]] + dp[i-1][j+nums[i-1]]
(数组第 i 位为 nums[i-1]),比如dp[x-1][j-nums[i-1]]
,我继续往后取一位,取到数组第 x 位时选择加它,就是成了dp[x][j-nums[i-1]+nums[i-1]]
也就是dp[x][j]
;我们的数组为 dp[][],他的长度根据 x 为数组前 x 位的定义,就定义为数组长度 n+1,最终结果为 dp[n][y],根据 y 的定义,y 的范围为-sum(nums)~sum(nums)
,所以 dp 定义为 dp[n+1][sum(nums)+1];定义完状态转移方程和边界条件,看一下初始条件,不然这个动态规划没法启动,永远一堆 0 加来加去,f(0,0) 为只计算数组前 0 位和为 0 的方案数,很明显只有一种,即 dp[0][0] 为 1。大致代码如下 -
public int findTargetSumWays(int[] nums, int target) { int sum = 0; for(int i:nums)sum+=i; itn n = nums.length; int[][] dp = new int[n+1][sum+1]; dp[0][0] = 1; for(int i=1;i<=n;i++){ int num = nums[i-1]; for(int j=-sum;j<=sum;j++){ dp[i][j] = dp[i-1][j-num]+dp[i-1][j+num]; } } return dp[n][target]; }
- 但是要知道,y 可以为负数,我数组下标可没有负数啊,也就是说为了代码正常运行,我们需要把 y 的范围整体右移一个 sum 。也就是把数组前 x 为和为 -1 有多少种可能存到 dp[x][-1+sum],和为 2 存到 dp[x][2+sum],同时排除掉之前没考虑的 y 不可能出现的值
-
public int findTargetSumWays(int[] nums, int target) { int sum = 0; for(int i:nums)sum+=i; // 全加都没你大或全减都没你小,那肯定没可能了 if(Math.abs(target)>sum){ return 0; } int n = nums.length; int[][] dp = new int[n+1][sum*2+1]; dp[0][0+sum] = 1; for(int i=1;i<=n;i++){ int num = nums[i-1]; for(int j=-sum;j<=sum;j++){ // 防止下标越界,实际含义就是:先去掉为了代码运行加上的偏移的 sum // 比如 j-num+sum>=0 实际上就是 j-m>= -sum,也就是 -sum <= j-num,因为 y 最小也只能为 -sum // 所以当 y 为 j-num 时,你也肯定大于等于 -sum,不可能比最小值还要小 // j+num+sum<=sum*2 去掉偏移得到 j+num <= sum,同理 y 为 j+num 时也不可能大于最大值 if(j-num+sum>=0)dp[i][j+sum] += dp[i-1][j-num+sum]; if(j+num+sum<=sum*2)dp[i][j+sum] += dp[i-1][j+num+sum];; } } return dp[n][target+sum]; }
- 但是以上考虑了很多无效的状态,我们知道动态规划就是下一步的状态值根据前一步得到(也就是状态转移),比如 sum 为 20, target 为 10,你在算到 y 为 20 时已经把 nums 里面的数都用完了,你这状态怎么再转移到 10,你已经没有下一步可以走了,也就是无法再进行状态转移。(个人理解,主要想说这个还能优化)首先我们知道,在我们组合后,这个数组就是一部分被我们加了起来,一部分被我们减了起来,比如这个数组的和为 20,我们要拿到 10,我们可能把一堆和为 15 的数加上加号,另一堆和为 5 的数加上了减号,最后
+15+(-5) = 10
,也就是我们划分出了正值部分和负值部分,我们把负值部分记作 m(注意负值部分也是一堆数相加,也就是说只用了加号),数组和 sum 简写为 s,正值部分也就为 s-m,参照+15+(-5) = 10
可以得到 +(s-m)+(-m) = target,也就是m = (s - target) / 2
,这时我们的问题可以转换为使用数组的一些数相加,能凑出和为 m 的方案数。重定义状态得到 f(x,y) 为取数组前 x 位,其中选几个数相加能凑出和为 y(y 就是负值部分) 的方案数,最终需求解 f(n,m);接下来状态转移方程也很容易得到,dp[i][j] = dp[i-1][j](之前就凑好了,现在这个 nums[i-1] 我就用不上了) + dp[i-1][j-nums[i-1]](比如我们要 dp[5][20],nums[4] 也就是数组第 5 个数为 10,dp[4][10] 再往后一步,选择为数组前五个数,相比之前多了一个选择为 nums[4],选择加上他正好能和我现在的 10 凑成 20),最终结果为dp[i][j]=dp[i−1][j]+dp[i−1][j−nums[i−1]]
-
public int findTargetSumWays(int[] nums, int t) { int s = 0; for(int i:nums)s+=i; // 因为 nums 里面都是非负整数,所以凑出来的 m 肯定也是非负整数,所以 s-t 一定为非负偶数 if(Math.abs(t)>s || (s - t) % 2 != 0){ return 0; } int m = (s-t)/2; int n = nums.length; int[][] dp = new int[n+1][m+1]; dp[0][0] = 1; for(int i=1;i<=n;i++){ int num = nums[i-1]; for(int j=0;j<=m;j++){ dp[i][j] += dp[i-1][j]; // 一堆非负整数凑出来的数当然大于等于 0 if(j-num>=0){ dp[i][j] += dp[i-1][j-num]; } } } return dp[n][m]; }