Q:给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。
返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
示例 1:
输入: 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位整数存下。
A:
1.递归
这个题虽然是典型的0-1背包,但由于有负数加入,实际上用递归比较好。
private int count;
public int findTargetSumWays(int[] nums, int S) {
count = 0;
calculate(nums, S, 0);
return count;
}
private void calculate(int[] nums, int S, int i) {
if (i == nums.length) {
if (S == 0) {
count++;
}
return;
}
calculate(nums, S - nums[i], i + 1);
calculate(nums, S + nums[i], i + 1);
}
2.动态规划
状态转移方程:
dp[i][j] = dp[i-1][j+nums[i]] + dp[i-1][j-nums[i]]
解释:j+nums[i]和j-nums[i]表示对nums[i]可以执行加,还可以执行减,那么dp[i][j]的结果值就是加/减之后对应位置的和。
明确base case:(关键)
例如,在示例nums = [1, 1, 1, 1, 1]中在非负整数数组的前提下,由于为是要选择+或者-,那么:
- 组成的最大和就是对所有元素全选+。sum = 5
- 组成的最小和的是对所有元素全选-。sum = -5
说明我们的目标和S一定是 -5<= S <=5的,其范围长度length = sum*2 + 1。这样就确定下状态了。那我们的dp表就是:
因为整个状态的范围长度length = sum*2 + 1,而j是从0开始的,故其初始化的位置应为sum。由于状态转移方程为dp[i][j] = dp[i-1][j+nums[i]] + dp[i-1][j-nums[i]].故dp表初始化应为:
//如果nums[0]==0的话,那么+0和-0都应该算作其操作,故if(nums[0] == 0) dp[0][sum] = 2;
if(nums[0] == 0) dp[0][sum] = 2;
else{
dp[0][sum+nums[0]] = 1;
dp[0][sum-nums[0]] = 1;
}
最后结果:
public int findTargetSumWays(int[] nums, int S) {
int sum = 0;
for (int num : nums)
sum += num;
if (Math.abs(S) > Math.abs(sum))
return 0;
int len = nums.length;
// - 0 +
int t = sum * 2 + 1;
int[][] dp = new int[len][t];
//初始化
if (nums[0] == 0) {
dp[0][sum] = 2;//0为正负
} else {
dp[0][sum + nums[0]] = 1;
dp[0][sum - nums[0]] = 1;
}
for (int i = 1; i < len; i++) {
for (int j = 0; j < t; j++) {
//注意边界
int l = (j - nums[i]) >= 0 ? j - nums[i] : 0;
int r = (j + nums[i]) < t ? j + nums[i] : 0;
dp[i][j] = dp[i - 1][l] + dp[i - 1][r];
}
}
return dp[len - 1][sum + S];
}