目录
链接
问题
给定一个非负整数数组,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 位整数存下。
题解
我的题解1: 递归
思路:
可以使用递归,枚举出所有可能的情况。具体地,当我们处理到第 i 个数时,我们可以将它添加 + 或 -,递归地搜索这两种情况。当我们处理完所有的 N 个数时,我们计算出所有数的和,并判断是否等于 S。
/**
* 递归
* 思路: nums数组中的每个元素都存在两种可能性: + 或者 -
*/
public int findTargetSumWays_Recu(int[] nums, int S) {
if(nums.length == 0) return 0;
return findTargetSumWays_(nums,-1,0,S);
}
public int findTargetSumWays_(int[] nums,int index, int S, int targetS) {
++ index;
/**
* 当计算完所有的数据, 比较当前S值与目标S值是否相同;
* 如果相同, 则证明该组合方式可用, 返回1, 否则该组合方法不可用, 返回0
*/
if(index >= nums.length){
if(targetS == S){
return 1;
}else{
return 0;
}
}
/**
* 递归查询满足 S == target 所需要剩余的组合方式
*/
return findTargetSumWays_(nums,index, S + nums[index],targetS)
+ findTargetSumWays_(nums,index, S - nums[index],targetS) ;
}
复杂度分析
时间复杂度: O(2^N),其中 N 是数组 nums 的长度
空间复杂度: O(N),为递归使用的栈空间大小
我的题解2: 递归 -- 优化
/**
* 递归 -- 优化
* 将剩余S的值进行进行计算( + 或者 - ), 直至数组nums中的所有数据均参加计算, 判断此时S是否为 0;
*/
public int findTargetSumWays_RecuBett(int[] nums, int S) {
if(nums.length == 0) return 0;
return findTargetSumWays_RecuBett(nums,-1,S);
}
public int findTargetSumWays_RecuBett(int[] nums,int index, int targetS) {
++ index;
/**
* 当计算完所有的数据, 比较当前S值是否为 0;
*/
if(index >= nums.length){
if(targetS == 0){
return 1;
}else{
return 0;
}
}
return findTargetSumWays_RecuBett(nums,index, targetS - nums[index])
+ findTargetSumWays_RecuBett(nums,index, targetS + nums[index]) ;
}
题解3: 动态规划
思路:
这道题也是一个常见的背包问题,可以用类似求解背包问题的方法来求出可能的方法数。
用 dp[i][j] 表示用数组中的前 i 个元素,组成和为 j 的方案数。考虑第 i 个数 nums[i],它可以被添加 + 或 -,因此状态转移方程如下:
dp[ i ][ j ] = dp[ i - 1 ][ j - nums[ i ] ] + dp[ i - 1 ][ j + nums[ i ] ]
也可以写成递推的形式:
dp[ i ][ j + nums[ i ] ] += dp[ i - 1 ][ j ]
dp[ i ][ j - nums[ i ] ] += dp[ i - 1 ][ j ]
由于数组中所有数的和不超过 1000,那么 j 的最小值可以达到 -1000。在很多语言中,是不允许数组的下标为负数的,因此我们需要给 dp[i][j] 的第二维预先增加 1000,即:
dp[ i ][ j + nums[ i ] + 1000] += dp[ i - 1][ j + 1000]
dp[ i ][ j - nums[ i ] + 1000] += dp[ i - 1][ j + 1000]
/**
* 动态规划
* 状态定义:
* dp(i,j): 使用数组前i个元素(包括i)可计算和为j的最多方法数
* 状态转移方程:
* dp[i][j] = dp[i - 1][j - nums[i]] + dp[i - 1][j + nums[i]]
* or
* dp[i][j + nums[i]] += dp[i - 1][j]
* dp[i][j - nums[i]] += dp[i - 1][j]
* 由于数组中所有数的和不超过 1000,那么 j 的最小值可以达到 -1000。
* 数组中是不允许下标为负数的,因此需要给 dp[i][j] 的第二维预先增加 1000,即:
* dp[i][j + nums[i] + 1000] += dp[i - 1][j + 1000]
* dp[i][j - nums[i] + 1000] += dp[i - 1][j + 1000]
*/
public int findTargetSumWays(int[] nums, int S) {
int[][] dp = new int[nums.length][2001];
/**
* 初始值: 使用前0个元素计算和为 nums[0]的数量为 1
* 如果nums[0] == 0, 那么 nums[0] + 1000 与 -nums[0] + 1000 的值一致, 所以需要+=
*/
dp[0][nums[0] + 1000] = 1;
dp[0][-nums[0] + 1000] += 1;
for (int i = 1; i < nums.length; i++) {
//使用nums数组的前i个数据, 可以组成sum的方法数 (根据题意给出的范围, 穷举所有的可能性)
for (int sum = -1000; sum <= 1000; sum++) {
/**
* 如果目标和为sum的方法数不为 0,即可以利用nums数组前i个元素组成此sum的值
*/
if (dp[i - 1][sum + 1000] > 0) {
dp[i][sum + nums[i] + 1000] += dp[i - 1][sum + 1000];
dp[i][sum - nums[i] + 1000] += dp[i - 1][sum + 1000];
}
}
}
return S > 1000 ? 0 : dp[nums.length - 1][S + 1000];
}
复杂度分析
时间复杂度: O(N∗sum),其中 N 是数组 nums 的长度。
空间复杂度: O(N∗sum)。
题解4: 动态规划 -- 空间复杂度优化
思路:
动态规划的状态转移方程中,dp[i][...]
只和 dp[i - 1][...]
有关,因此我们可以优化动态规划的空间复杂度,只需要使用两个一维数组即可
/**
* 动态规划 -- 空间复杂度优化
*/
public int findTargetSumWays_Better(int[] nums, int S) {
int[] dp = new int[2001];
dp[nums[0] + 1000] = 1;
dp[-nums[0] + 1000] += 1;
for (int i = 1; i < nums.length; i++) {
int[] next = new int[2001];
for (int sum = -1000; sum <= 1000; sum++) {
if (dp[sum + 1000] > 0) {
next[sum + nums[i] + 1000] += dp[sum + 1000];
next[sum - nums[i] + 1000] += dp[sum + 1000];
}
}
dp = next;
}
return S > 1000 ? 0 : dp[S + 1000];
}
复杂度分析
时间复杂度:O(N∗sum),其中 N 是数组 nums 的长度。
空间复杂度:O(sum)。