文章目录
- 题目描述
- 思路 && 代码
- 1. 动态规划 O(n2n^2n2)、O(n2n^2n2)(最方便理解,初版)
- 2. 转换成 01 背包问题 O(n2n^2n2)、O(nnn)
- 二刷
- 离谱!添加了测试用例,上面的代码需要添加负数条件了(见下面的代码)
打卡第十五天~继续加油!
题目描述
- 和上一道分割等和子集的做法很像
- 这里代码迭代了很多次,但是时间复杂度是一样的,只是空间复杂度、耗时不断优化。
思路 && 代码
1. 动态规划 O(n2n^2n2)、O(n2n^2n2)(最方便理解,初版)
- dp[i][j]:下标[0 ~ i]构成的数集,能得到 j - sum 的情况种数
- 因为nums[ ]可构成元素范围为[-sum, sum],因此开出[2 * sum + 1]的列数组。
其中[0] 代表 -sum,[2 * sum] 代表 [sum],以此类推。 - 注意:初始化时,nums[0] 可能等于 -nums[0],因此要用到 +=,而非 = 。
- 状态转移:对于当前的 j,分成 +nums[i],和-nums[i]的情况,对上一行的值进行选取即可。
- 缺陷:空间复杂度不方便通过滚动数组的方式进行优化,因为状态转移方程的过程中,不但用到了前面的元素,还用到的后面的元素。解决方法:转换成01背包问题
class Solution {public int findTargetSumWays(int[] nums, int target) {int sum = 0;for(int temp : nums) {sum += temp;}// 全正 or 全负,不在范围的情况if(sum < target || -sum > target) {return 0;}// dp[i][j]:下标[0 ~ i]构成的数集,能得到 j - sum 的情况种数int top = 2 * sum + 1;int[][] dp = new int[nums.length][top];// 初始化:只取第一个元素,只能给 nums[0] 和 -nums[0] 带来 1 个种数dp[0][sum + nums[0]] = 1;// 注意:存在 nums[0] == -nums[0] 的情况,因此这边要用 +=dp[0][sum - nums[0]] += 1;// 状态转移for(int i = 1; i < nums.length; i++) {for(int j = 0; j < top; j++) {// Case 1:第 i 个元素取 + if(j >= nums[i]) {dp[i][j] = dp[i - 1][j - nums[i]];}// Case 2: 第 i 个元素取 -if(j + nums[i] < top) {dp[i][j] += dp[i - 1][j + nums[i]];}}}return dp[nums.length - 1][target + sum];}
}
2. 转换成 01 背包问题 O(n2n^2n2)、O(nnn)
- 实际上,题目可以这样转换成01背包问题:把 - 当成 0,不选;把 + 当成 1,选。
- 原本的(-nums) + (+nums) == target,表达式左边和右边都加上 sum,就转换成
0 + 2 * (+nums) == sum + target,方便起见,我们可以再进行除2操作,变成
+nums == (sum + target) / 2。 - 注意:由此可推出,如果(sum + target) 为奇数,说明不存在对应的 +nums 序列,也就是不可取。
- 接下来就可以正常地进行 01背包 的动态规划了~
- 滚动数组:逆序,现在没有减法的情况下,可以保证无后效性
class Solution {public int findTargetSumWays(int[] nums, int target) {int sum = 0;for(int temp : nums) {sum += temp;}// 不在范围的情况 && 奇数无法匹配到选取方式(可证)if(sum < target || (sum + target) % 2 == 1) {return 0;}// 转换成 01背包:-号转成不取;+号转成取,两倍// 实际上,只要考虑到 target + sum 即可,后面的和结果无关int top = (sum + target) / 2 + 1;// dp[i][j]:下标[0 ~ i]构成的数集,能得到 j - sum 的情况种数int[] dp = new int[top];// 初始化:第一个元素不取,只能给 0 带来 1 个种数dp[0] = 1;// 状态转移for(int i = 0; i < nums.length; i++) {for(int j = top - 1; j >= nums[i]; j--) {// Case 1:取第 i 个元素dp[j] += dp[j - nums[i]];// Case 2: 不取第 i 个元素(一维情况下相当于不用考虑)}}return dp[top - 1];}
}
- 来个无注释版代码吧,方便直接看代码:
class Solution {public int findTargetSumWays(int[] nums, int target) {int sum = 0;for(int temp : nums) {sum += temp;}if(sum < target || (sum + target) % 2 == 1) {return 0;}int top = (sum + target) / 2 + 1;int[] dp = new int[top];dp[0] = 1;for(int i = 0; i < nums.length; i++) {for(int j = top - 1; j >= nums[i]; j--) {dp[j] += dp[j - nums[i]];}}return dp[top - 1];}
}
二刷
离谱!添加了测试用例,上面的代码需要添加负数条件了(见下面的代码)
class Solution {public int findTargetSumWays(int[] nums, int target) {// 转换成背包:+取两次,-不取。target 相当于加了一次 sumfor(int temp : nums) {target += temp;}// 偶数之和不能为奇数 || 非负数之和不能为负if(target % 2 == 1 || target < 0) {return 0;}target /= 2;int[] dp = new int[target + 1];// 边界处理:0的组合法有一个(都不取)dp[0] = 1;for(int i = 0; i < nums.length; i++) {for(int j = target; j >= nums[i]; j--) {// 相当于,这一轮的结果 = 上一轮的结果 + 这一轮的添加dp[j] += dp[j - nums[i]];}}return dp[target];}
}
- 无注释版,11行有效代码
class Solution {public int findTargetSumWays(int[] nums, int target) {for(int temp : nums) {target += temp;}if(target % 2 == 1 || target < 0) {return 0;}target /= 2;int[] dp = new int[target + 1];dp[0] = 1;for(int i = 0; i < nums.length; i++) {for(int j = target; j >= nums[i]; j--) {dp[j] += dp[j - nums[i]];}}return dp[target];}
}