文章目录
打卡第十五天~继续加油!
题目描述
- 和上一道分割等和子集的做法很像
- 这里代码迭代了很多次,但是时间复杂度是一样的,只是空间复杂度、耗时不断优化。
思路 && 代码
1. 动态规划 O( n 2 n^2 n2)、O( n 2 n^2 n2)(最方便理解,初版)
- 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( n 2 n^2 n2)、O( n n n)
- 实际上,题目可以这样转换成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 相当于加了一次 sum
for(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];
}
}