LeetCode 494.目标和
难度:中等
给定一个非负整数数组,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 位整数存下。
解题思路:
方法一:回溯
数组 nums 的每个元素都可以添加符号 + 或-,因此每个元素有 2 种添加符号的方法,n 个数共有 2^n
种添加符号的方法,对应 2^n 种不同的表达式。当 n 个元素都添加符号之后,即得到一种表达式,如果表达式的结果等于目标数 target,则该表达式即为符合要求的表达式。
可以使用回溯的方法遍历所有的表达式,回溯过程中维护一个计数器 count,当遇到一种表达式的结果等于目标数 target 时,将 count 的值加 1。遍历完所有的表达式之后,即可得到结果等于目标数 target 的表达式的数目。
public int findTargetSumWays(int[] nums, int target) {
backtrack(nums, target, 0, 0);
return count;
}
public void backtrack(int[] nums, int target, int index, int sum) {
if (index == nums.length) {
if (sum == target) {
count++;
}
} else {
backtrack(nums, target, index + 1, sum + nums[index]);
backtrack(nums, target, index + 1, sum - nums[index]);
}
}
}
复杂度分析
时间复杂度:O(2n),其中 nnn 是数组 nums 的长度。回溯需要遍历所有不同的表达式,共有 2^n种不同的表达式,每种表达式计算结果需要 O(1)的时间,因此总时间复杂度是 O(2^n)。
空间复杂度:O(n),其中 nnn 是数组 nums 的长度。空间复杂度主要取决于递归调用的栈空间,栈的深度不超过 n。
方法二:动态规划
这里如何转化为01背包问题呢。
假设加法的总和为x,那么减法对应的总和就是sum - x。
比如说[1,1,1,1,1]这个例子,target为3,那我们的加法总和为1+1+1+1,减法总和为1,分成两部分
所以我们要求的是 x - (sum - x) = target
x = (target + sum) / 2
此时问题就转化为,装满容量为x的背包,有几种方法。
这里的x,就是bagSize,也就是我们后面要求的背包容量。
大家看到(target + sum) / 2 应该担心计算的过程中向下取整有没有影响。
这么担心就对了,例如sum 是5,S是2的话其实就是无解的,所以:
if ((S + sum) % 2 == 1) return 0; // 此时没有方案
同时如果 S的绝对值已经大于sum,那么也是没有方案的。
if (abs(S) > sum) return 0; // 此时没有方案
再回归到01背包问题,为什么是01背包呢?
因为每个物品(题目中的1)只用一次!
这次和之前遇到的背包问题不一样了,之前都是求容量为j的背包,最多能装多少。
本题则是装满有几种方法。其实这就是一个组合问题了。
- 确定dp数组以及下标的含义
dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法
- 确定递推公式
有哪些来源可以推出dp[j]呢?
只要搞到nums[i],凑成 j 就有dp[j - nums[i]] 种方法。
例如:dp[j],j 为4,
- 已经有一个1(nums[i]) 的话,有 dp[4-1]种方法 凑成 容量为4的背包
- 已经有一个2(nums[i]) 的话,有 dp[4-2]种方法 凑成 容量为4的背包
- 已经有一个3(nums[i]) 的话,有 dp[4-3]种方法 凑成 容量为4的背包
- 已经有一个4(nums[i]) 的话,有 dp[4-4]种方法 凑成 容量为4的背包
那么凑整dp[4]有多少方法呢,也就是把 所有的 dp[j - nums[i]] 累加起来。
所以求组合类问题的公式,都是类似这种:
dp[j] += dp[j - nums[i]]
这个公式在后面在讲解背包解决排列组合问题的时候还会用到!
- dp数组如何初始化
从递推公式可以看出,在初始化的时候dp[0] 一定要初始化为1,因为dp[0]是在公式中一切递推结果的起源,如果dp[0]是0的话,递推结果将都是0。
手动推导
这里我理解的是,首先选出nums[0],nums[0]的值为1
- 背包容量为5的时候,当前只有重量1的物品,有dp[5]+dp[5-1]种方法
- 背包容量为4的时候,当前只有重量1的物品,有dp[4]+dp[4-1]种方法
- 背包容量为3的时候,当前只有重量1的物品,有dp[3]+dp[3-1]种方法
- 背包容量为2的时候,当前只有重量1的物品,有dp[2]+dp[2-1]种方法
- 背包容量为1的时候,当前只有重量1的物品,有dp[1]+dp[1-1]种方法
对于每个 j
值,我们将 dp[j]
累加上 dp[j - nums[i]]
的值。这是因为我们可以选择使用当前元素 nums[i]
或不选择它,以得到和为 j
。通过累加 dp[j - nums[i]]
的方案数量,我们将选择了其他数字得到和为 j - nums[i]
的方案数量加到了选择了当前数字得到和为 j
的方案数量中,以确保我们考虑了所有可能的选择。
在选择出nums[1]的基础上,又有了nums[2]
- 背包容量为5的时候,当前有重量1和2的物品,有dp[5]+dp[5-2]种方法
- 背包容量为4的时候,当前有重量1和2的物品,有dp[4]+dp[4-2]种方法
- 背包容量为3的时候,当前有重量1和2的物品,有dp[3]+dp[3-2]种方法
- 背包容量为2的时候,当前有重量1和2的物品,有dp[2]+dp[2-2]种方法
在选择出nums[1],nums[2]的基础上,又有了nums[3]
- 背包容量为5的时候,当前有重量1,2,3的物品,有dp[5]+dp[5-3]种方法
- 背包容量为4的时候,当前有重量1,2,3的物品,有dp[4]+dp[4-3]种方法
- 背包容量为3的时候,当前有重量1,2,3的物品,有dp[3]+dp[3-3]种方法
这里的话举个例子:拿第一条来说,dp[5]+dp[5-3]种方法,指的是:
当我的背包此时已经装了nums[1],nums[2]的物品,
我要装满5的背包容量,有几种装法,一种是装nums[3](即dp[5-3]),一种是不装nums3(即dp[5]),这样会好理解一些。最后我们整理思考一下,也就是我之前装满背包为2的方法有dp[5-3]种方法,我现在有了nums[3],想要装满背包为5的方法有dp5(不装nums[3],这里不装nums[3]显然是凑不成5的)+dp[5-3](装nums[3],这里一旦装了nums[3],就凑成5,也就是dp[2]种方法,解释的有点乱,大家可以思考一下)
代码实现:
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 ( target < 0 && sum < -target) return 0;
if ((target + sum) % 2 != 0) return 0;
int size = (target + sum) / 2;
if(size < 0) size = -size;
int[] dp = new int[size + 1];
dp[0] = 1;
for (int i = 0; i < nums.length; i++) {
for (int j = size; j >= nums[i]; j--) {
////本层装满方案数=不放i物品装满方案数 + 放i物品装满方案数
dp[j] =dp[j] + dp[j - nums[i]];
}
}
return dp[size];
}
}
代码跟踪:
/**
* 举例推导dp数组
* 输入:nums: [1, 1, 1, 1, 1], S: 3
*
* bagSize = (S + sum) / 2 = (3 + 5) / 2 = 4
* @param nums
* @param target
* @return
*/
---------------(i = 0)-------------------
dp[4]+=dp[4-nums[0]]
dp[4-nums[0]]=0
nums[0]=1
dp[4]=0
dp[3]+=dp[3-nums[0]]
dp[3-nums[0]]=0
nums[0]=1
dp[3]=0
dp[2]+=dp[2-nums[0]]
dp[2-nums[0]]=0
nums[0]=1
dp[2]=0
dp[1]+=dp[1-nums[0]]
dp[1-nums[0]]=1
nums[0]=1
dp[1]=1
---------------(i = 1)-------------------
dp[4]+=dp[4-nums[1]]
dp[4-nums[1]]=0
nums[1]=1
dp[4]=0
dp[3]+=dp[3-nums[1]]
dp[3-nums[1]]=0
nums[1]=1
dp[3]=0
dp[2]+=dp[2-nums[1]]
dp[2-nums[1]]=1
nums[1]=1
dp[2]=1
dp[1]+=dp[1-nums[1]]
dp[1-nums[1]]=1
nums[1]=1
dp[1]=2
---------------(i = 2)-------------------
dp[4]+=dp[4-nums[2]]
dp[4-nums[2]]=0
nums[2]=1
dp[4]=0
dp[3]+=dp[3-nums[2]]
dp[3-nums[2]]=1
nums[2]=1
dp[3]=1
dp[2]+=dp[2-nums[2]]
dp[2-nums[2]]=2
nums[2]=1
dp[2]=3
dp[1]+=dp[1-nums[2]]
dp[1-nums[2]]=1
nums[2]=1
dp[1]=3
---------------(i = 3)-------------------
dp[4]+=dp[4-nums[3]]
dp[4-nums[3]]=1
nums[3]=1
dp[4]=1
dp[3]+=dp[3-nums[3]]
dp[3-nums[3]]=3
nums[3]=1
dp[3]=4
dp[2]+=dp[2-nums[3]]
dp[2-nums[3]]=3
nums[3]=1
dp[2]=6
dp[1]+=dp[1-nums[3]]
dp[1-nums[3]]=1
nums[3]=1
dp[1]=4
---------------(i = 4)-------------------
dp[4]+=dp[4-nums[4]]
dp[4-nums[4]]=4
nums[4]=1
dp[4]=5
dp[3]+=dp[3-nums[4]]
dp[3-nums[4]]=6
nums[4]=1
dp[3]=10
dp[2]+=dp[2-nums[4]]
dp[2-nums[4]]=4
nums[4]=1
dp[2]=10
dp[1]+=dp[1-nums[4]]
dp[1-nums[4]]=1
nums[4]=1
dp[1]=5
5
Process finished with exit code 0
评论区