目标和
题目
版本1 正确
public int findTargetSumWays(int[] nums, int target) {
// 数组中每个数字, 都可以选择是赋予正数还是负数, 目的是等于target
// 统计有多少种方案
// 可以看作是完全背包问题, 每个数字我可以放正数也可以放负数, 就相当于背包我可以选择放, 也可以选择不放
// dp[i][j] 数组的定义就是, 一定使用前i个元素, 构成目标金额j的方案数目
// dp[i][j] = dp[i - 1][j - nums[i]] + dp[i - 1][j + nums[i]]
// 根据状态转移方程, 我们发现当前状态是和未来的j是有关的, 此时可以采用递归, 或者改写成递推的写法
// 即dp[i][j + nums[i]] = dp[i - 1][j] + dp[i][j + nums[i]] 这里为什么要加上dp[i][j + nums[i]]呢?
// 是因为j + nums[i] 的结果, 可能会由多个nums[i]得到, 因此必须加上之前的结果
// 发现列存在减法的情况, 很有可能小于0, 因此需要为列增加空间
// 根据题目, nums的和最大为1000, 也就是j最多就是减去1000
// 因此相当于目标金额是把1000当作0的, 1000是作为基础值的
int [][] dp = new int[nums.length][2001];
// base case
dp[0][nums[0] + 1000] = 1; // 第一个元素, 构成自己的正数方案数为1
dp[0][1000 - nums[0]] += 1; // 第一个元素, 构成自己的负数方案数为 +=1 原因是第一个数可能为0
for (int i = 1; i < nums.length; i ++) {
for (int j = - 1000; j <= 1000; j ++) {
// dp[i - 1][j + 1000]的判断就是大于0才有累加的意义, 不然也是加0
if (dp[i - 1][j + 1000] > 0) {
dp[i][j + nums[i] + 1000] += dp[i - 1][j + 1000];
dp[i][j - nums[i] + 1000] += dp[i - 1][j + 1000];
}
}
}
return dp[nums.length - 1][target + 1000];
}
正确的原因
(1) 注意目标金额那里全部都增加了1000
(2) 枚举目标金额在-1000到1000的所有情况
(3) 状态转移方程, 如何变成递推的方程
版本2 错误 将问题转化
public int findTargetSumWays(int[] nums, int target) {
// 数组中每个数字, 都可以选择是赋予正数还是负数, 目的是等于target
// 统计有多少种方案
// 假设为数组中所有元素都分配了正号和负号, 那么数组nums中的元素, 此时就为两部分, 一部分是正的, 一部分是负的
// 此时正数部分的和假设为X, 负数部分的和假设为Y, 定义sum是数组元素全为正的时候的和(即原始nums数组的和)
// 存在 X - Y = sum;
// 那么假设此时数组是我们期望的数组 也存在 X + Y = target
// 对X - Y = sum 以及X + Y = target进行变量替换, 将Y消除, 得到 x = (sum + target) / 2
// 那么问题就变成在nums中, 寻找一个子序列数组, 满足该子序列的和x = (sum + target) / 2
// 就变成了单纯的背包问题
int sum = 0;
for (int i = 0; i < nums.length; i ++) {
sum += nums[i];
}
// 目标和不能大于sum, 否则全为正数也组成不了
// 同时子序列的和x一定要是整数, 因此(target + sum) % 2一定要是整数
if (target > sum || (target + sum) % 2 != 0) {
return 0;
}
sum = (sum + target) / 2;
// dp[i][j] 表示在数组nums中选择前i个元素, 能够构成j的种数
// 注意这里前i个元素 也需要为dp多一个位置, 背包问题数组第一个位置都应该是nums.length + 1
int [][] dp = new int [nums.length + 1][sum + 1];
// base case dp[i][0] = 1
for (int i = 0; i < nums.length + 1; i ++) {
dp[i][0] = 1;
}
for (int i = 1; i < nums.length + 1; i ++) {
for (int j = 1; j < sum + 1; j ++) {
if (j - nums[i - 1] >= 0) {
dp[i][j] = dp[i - 1][j - nums[i - 1]] + dp[i - 1][j];
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[nums.length][sum];
}
错误的原因
(1) 数组中可能会出现0, 但是正负0认为是两种情况, 因此需要在base case那里, 针对0的情况做处理
版本3 最优答案
public int findTargetSumWays(int[] nums, int target) {
// 数组中每个数字, 都可以选择是赋予正数还是负数, 目的是等于target
// 统计有多少种方案
// 假设为数组中所有元素都分配了正号和负号, 那么数组nums中的元素, 此时就为两部分, 一部分是正的, 一部分是负的
// 此时正数部分的和假设为X, 负数部分的和假设为Y, 定义sum是数组元素全为正的时候的和(即原始nums数组的和)
// 存在 X - Y = sum;
// 那么假设此时数组是我们期望的数组 也存在 X + Y = target
// 对X - Y = sum 以及X + Y = target进行变量替换, 将Y消除, 得到 x = (sum + target) / 2
// 那么问题就变成在nums中, 寻找一个子序列数组, 满足该子序列的和x = (sum + target) / 2
// 就变成了单纯的背包问题
int sum = 0;
for (int i = 0; i < nums.length; i ++) {
sum += nums[i];
}
// 目标和不能大于sum, 否则全为正数也组成不了
// 同时子序列的和x一定要是整数, 因此(target + sum) % 2一定要是整数
if (target > sum || (target + sum) % 2 != 0) {
return 0;
}
sum = (sum + target) / 2;
// dp[i][j] 表示在数组nums中选择前i个元素, 能够构成j的种数
// 注意这里前i个元素 也需要为dp多一个位置, 背包问题数组第一个位置都应该是nums.length + 1
int [][] dp = new int [nums.length + 1][sum + 1];
// base case dp[i][0] = count
int count = 1;
dp[0][0] = 1; // 即[]
for (int i = 1; i < nums.length + 1; i ++) {
if (nums[i - 1]== 0) {
// 如果某个元素为0, 那么[+0]和[-0]都满足最后重量为0的的条件
// 如果元素不为0, 那么种数就和之前一致
// 如果遇见第二个0, 种数就要继续乘2
count *= 2;
}
dp[i][0] = count;
}
for (int i = 1; i < nums.length + 1; i ++) {
for (int j = 1; j < sum + 1; j ++) {
if (j - nums[i - 1] >= 0) {
dp[i][j] = dp[i - 1][j - nums[i - 1]] + dp[i - 1][j];
} else {
dp[i][j] = dp[i - 1][j];
}
}
}
return dp[nums.length][sum];
}
正确的原因
(1) 一定要注意base case的写法, 数组可能有0, 因此目标金额为0的情况是有不同的种数的, 而不是固定为1