LeetCode_121_TargetSum


1. question: 目标和(中等)

给你一个整数数组 nums 和一个整数 target 。

向数组中的每个整数前添加 ‘+’ 或 ‘-‘ ,然后串联起所有整数,可以构造一个 表达式 :

例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-‘ ,然后串联起来得到表达式 “+2-1” 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。

来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/target-sum

示例 1:

1
2
3
4
5
6
7
8
输入: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:

1
2
输入:nums = [1], target = 1
输出:1

提示:

1
2
3
4
1 <= nums.length <= 20
0 <= nums[i] <= 1000
0 <= sum(nums[i]) <= 1000
-1000 <= target <= 1000

2. answers

这道题经过自己思考,终于做出来了。先讲解一下自己的做法,在对比一下题解。

首先,这道题要求所能形成target的所有组合数。因为加法和减法的存在,所以以动态规划的思想来看的话,当前元素下的target必定由两种情况组成(和爬楼梯类似):

  1. 上一个元素的 target为【target - 当前元素值】的情况,表明当上一个元素的target为【target - 当前元素值】时的所有种类,因为该target + 本元素值,就是本target了,即该情况有多少种方式,那么本情况就有多少种方式
  2. 上一个元素的target为【target +当前元素值】的情况,表明当上一个元素的target为【target + 当前元素值】时的所有种类,因为该target - 本元素值,就是本target了,即该情况有多少种方式,那么本情况就有多少种方式

所以,可设置数组,dp[i][j],表明,在容量J的情况下,当前元素及其前面所有元素,所能形成总和为J的表达式的种类。背包容量就是所有的总和取值范围,即[-总和, 总和]。但是为了下标,需要将其映射到正空间[0, 2*总和+1),那么仍然需要保存背包容量为0的时刻(这里的0不是总和为0,而是不装元素的0),所以背包总范围是[0,2*总和+2)。

物品就是数组,weight就是自身nums[i],value也是自身nums[i]。

需要初始化背包为0的情况;需要初始化仅取第一个元素的情况,注意,这里需要注意,因为target可以是正负数,因此,当target绝对值等于nums[0]时,赋值为1,表明有一种方式形成该总value。

对于情况目标值为0的情况需要特殊考虑,这里的0是下标为[sums+1]时的0,如果nums[0]也为0,此时既可以是加法,也可以是减法,即这种情况需要赋值为2。

最终返回的结果就是dp[nums.leng-1][target+sums+1],即实际总和为target时的所有情况。代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
public class Solution_0121_02 {

public int findTargetSumWays(int[] nums, int target) {

int sums = 0;

for(int i: nums) sums += i;

if(Math.abs(target) > sums) return 0;

// 因为target可能是负数,所以此时的所有数值区间就是 [-sums, sums],对于下标就是[-sums, sums + 1)
// 但是为了方便数组下标,并且背包容量不可能是负数,所以区间统一向后移动(sums + 1)个位置,即[1, 2*sums+2)
// 对于[0],意味着不放的情况下。
int bagSize = 2 * sums + 2;

int[][] dp = new int[nums.length][bagSize];

// 初始化,在背包尺寸为0的情况下(其实数组已经初始化为0了,这里没必要,但是为了逻辑上更加清楚,简单初始化一下)
for (int i = 0; i < nums.length; i++) {
dp[i][0] = 0;
}

// 初始化,在任何尺寸下,仅选取第一个数字的时候的所能达到的当前容量的组合个数。
// 注意下标,因为统一移动了 sums + 1,所以比较的时候,需要将其返回原数。
for (int i = 0; i < bagSize; i++) {

// TODO 注意,这里有点问题,如果 nums[0] = 0,那么该数 在 背包容量为0(不是不装,而是 0+sums+1)的时候,
// 所能形成的种类其实有两个,即 +0 和 -0,这种情况是特殊的。

if(nums[0] == 0 && i - sums - 1 == 0) dp[0][i] = 2;
else if(nums[0] == Math.abs(i - sums - 1)) dp[0][i] = 1;
}

// 动态规划,遍历数组中每种情况
for (int i = 1; i < nums.length; i++) {

for (int j = 1; j < bagSize; j++) {

// 在当前容量的情况下,以及在当前元素及其前面元素的情况下,目标值为j,仅有两种情况
// 1. j - nums[i] 的种类。即dp[i-1][j - nums[i]]
// 2. j + nums[i] 的种类。即dp[i-1][j + nums[i]]

// 注意,因为统一后移了 sums + 1 个位置,所以此时的j,所表示的值 实际上是 j - sums - 1
// 但是因为统一后移了,所以此时,无需考虑下标的问题。
int pre = 0;
int next = 0;
if(j - nums[i] >= 0) {
pre = dp[i-1][j - nums[i]];
}
if(j + nums[i] < bagSize) {
next = dp[i-1][j + nums[i]];
}

dp[i][j] = pre + next;
}
}

// 此时动态规划完所有情况
// 那么最终结果就是,dp[nums.length - 1][target + sums + 1]的值。
// 即在当前容量下,选取当前及以前容量,所能达到当前容量的最大种类数。

return dp[nums.length - 1][target + sums + 1];
}

public static void main(String[] args) {

// int[] nums = {1,1,1,1,1};
// int target = 3;

int[] nums = {1};
int target = 2;

// int[] nums = {0,0,0,0,0,0,0,0,1};
// int target = 1;

Solution_0121_02 s = new Solution_0121_02();

System.out.println(s.findTargetSumWays(nums, target));
}
}

参考了题解,其实还有另一种方式。无论表达式是什么形式,最终都可转换为A-B=target,而因为A+B=sum,显然题目就是求解AB对的数量,又因为B=(sum-target)/2,所以就是求解和为B的组合数。

因为给定的数组中均为整数,显然B一定是整数,也就是一定能整除,如果不能整除,显然是没有解的。另外,B也不能是负数,或者target<-sum,即Math.abs(target)<=sum,如果违反了这个条件也是没有解的。

那么递推公式什么样子呢?显然,和为B的组合数,如果当前元素x小于B,那么当前组合数就是和为[B-x]的组合数(因为这种组合加上当前数刚好是B,即装本元素),还有上一个元素何为B的组合数(不装本元素),是两种情况之和。

注意,可以一维动态数组来存储数值,但是初始化的时候要注意一下。

如果nums[i]=0,并且dp[j],即j=0,也就是当前组合为0的情形,显然这时候有两种方式,即选不选都是0;

另外,如果nums[i] == j,显然是一种方式。

那么其他时候还需要赋值吗(不是0)?还有一种就是当j=0,但是nums[i]不是0的时候,显然不取该元素就行,是一种方式,即赋值为1。

代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
public class Solution_0121_03 {

public int findTargetSumWays(int[] nums, int target) {

int sums = 0;

for(int i: nums) sums += i;

int bagSize = sums - target;

if(bagSize % 2 != 0) return 0;
if(Math.abs(target) > sums) return 0;

bagSize = bagSize / 2;

// 注意,除了 bagSize + 1之外,这里加一,指的是没有元素
int[] dp = new int[bagSize + 1];

// 初始化
// 如果元素[0]等于当前背包容量,显然,取它刚好满足,即是一种方式。
// 如果J=0,但是元素[0]不等于0,显然需要赋值为1,即是一种方式,不取该元素
// 如果J=0,但是元素[0]等于0,显然需要赋值为2,这是两种方式,即取不取该元素都满足背包容量
for (int i = 0; i < dp.length; i++) {
if(nums[0] == 0 && i == 0) dp[i] = 2;
else if(i == 0) dp[i] = 1;
else if(nums[0] == i) dp[i] = 1;
}

// 因为计算总和是否为bagSize,所以都是整数,
for (int i = 1; i < nums.length; i++) {

for (int j = dp.length - 1; j >= 0 ; j--) {

// 如果当前元素不能装入,那么只能是前一个元素的结果
if(j < nums[i]) dp[j] = dp[j];

// 如果当前元素可以装入,那么就是 装入本元素的结果(等于前面容量的结果) + 不装本元素的结果(前一个元素的结果)
else dp[j] = dp[j-nums[i]] + dp[j];
}

}

// 最终
return dp[bagSize];
}

public static void main(String[] args) {

// int[] nums = {1,1,1,1,1};
// int target = 3;

// int[] nums = {1};
// int target = 2;

// int[] nums = {0,0,0,0,0,0,0,0,1};
// int target = 1;

int[] nums = {9,7,0,3,9,8,6,5,7,6};
int target = 2;

Solution_0121_03 s = new Solution_0121_03();

System.out.println(s.findTargetSumWays(nums, target));
}
}

做到这里,似乎背包问题:

  1. 将问题转换为背包问题
  2. 确定数组含义
  3. 确定递推公式
  4. 对数组初始化

上面的每一步都很重要,但是最重要的还是如何将问题转换为背包问题。比如本题,第一种方式和第二种方式完全是两种不同的思维。

3. 备注

参考力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台 (leetcode-cn.com)代码随想录 (programmercarl.com)


文章作者: 浮云
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 浮云 !
  目录