题目描述
给你一个整数数组 nums 和一个整数 target 。
向数组中的每个整数前添加 ‘+’ 或 ‘-’ ,然后串联起所有整数,可以构造一个 表达式 :
例如,nums = [2, 1] ,可以在 2 之前添加 ‘+’ ,在 1 之前添加 ‘-’ ,然后串联起来得到表达式 “+2-1” 。
返回可以通过上述方法构造的、运算结果等于 target 的不同 表达式 的数目。
思路
这道题最简单的一个思路就是回溯穷举,实现很简单,这里不多赘述,缺点是时间复杂度非常高。下面我们会讲到另外一种思路。
动态规划求解
说实话这道题动态规划我是没啥思路,一方面是我目前对动态规划掌握的确实不深,这道题的状态转移方程想不出来。
我们看看用动态规划怎么求解吧:
首先这个问题可以转化为子集划分问题,而子集划分问题又是一个经典的背包问题。。。。动态规划就是这样玄学
首先,如果我们把nums划分为两个子集A和B的话,分别代表分配+的数和分配-的数,那么他们和target存在如下关系:
sum(A) - sum(B) = target
sum(A) = target + sum(B)
sum(A) + sum(A) = target + sum(B) + sum(A)
2 * sum(A) = target + sum(nums)
可以推出sum(A) = (target + sum(nums)) / 2,然后问题可以转化为:nums中存在几个子集,使得A中的元素和为 (target + sum(nums)) / 2。
变成背包问题的标准形式:
有一个背包,容量为 sum,现在给你 N 个物品,第 i 个物品的重量为 nums[i - 1](注意 1 <= i <= N),每个物品只有一个,请问你有几种不同的方法能够恰好装满这个背包?
下面开始解题:
第一步:明确【状态】和【选择】
对于背包问题的话,状态是【背包的容量】和【可选择的物品】,选择就是【装进背包】和【不装背包】
第二步:明确dp数组的定义
dp[i][j] = x表示:若只在前i个物品中选择,若当前背包的容量为j,则有最多x种方法可以恰好装满背包
翻译成我们这道题就是:只在前i个数中选择,若目标和为j,有x中办法划分子集
base case:dp[0][…]为0,因为没有物品的话,没有办法填充背包,dp[…][0]为1,因为若背包容量为0,什么都不装就是唯一方案
第三步:根据【选择】,思考状态转移的逻辑
如果不把nums[i]算入子集,那么恰好装满背包的背包的方案数就取决于上一个状态dp[i-1][j]。
如果nums[i]算入子集,那么只要看前i-1种物品有几种方法可以装满j-nums[i-1]的重量就行了,所以取决于状态dp[i-1][j-nums[i-1]]
PS:注意我们说的 i 是从 1 开始算的,而数组 nums 的索引时从 0 开始算的,所以 nums[i-1] 代表的是第 i 个物品的重量,j - nums[i-1] 就是背包装入物品 i 之后还剩下的容量。
状态转移方程为:dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]]
这里我们还可以做下优化,因为dp[i][j]只和前面dp[i-1][…]有关,所以可以优化为一维dp,代码如下:
/* 计算 nums 中有几个子集的和为 sum */
int subsets(int[] nums, int sum) {
int n = nums.length;
int[] dp = new int[sum + 1];
// base case
dp[0] = 1;
for (int i = 1; i <= n; i++) {
// j 要从后往前遍历
for (int j = sum; j >= 0; j--) {
// 状态转移方程
if (j >= nums[i-1]) {
dp[j] = dp[j] + dp[j-nums[i-1]];
} else {
dp[j] = dp[j];
}
}
}
return dp[sum];
}
二维dp代码如下:
/* 计算 nums 中有几个子集的和为 sum */
int subsets(int[] nums, int sum) {
int n = nums.length;
int[][] dp = new int[n + 1][sum + 1];
// base case
for (int i = 0; i <= n; i++) {
dp[i][0] = 1;
}
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= sum; j++) {
if (j >= nums[i-1]) {
// 两种选择的结果之和
dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i-1]];
} else {
// 背包的空间不足,只能选择不装物品 i
dp[i][j] = dp[i-1][j];
}
}
}
return dp[n][sum];
}
对照二维 dp,只要把 dp 数组的第一个维度全都去掉就行了,唯一的区别就是这里的 j 要从后往前遍历,原因如下:
因为二维压缩到一维的根本原理是,dp[j] 和 dp[j-nums[i-1]] 还没被新结果覆盖的时候,相当于二维 dp 中的 dp[i-1][j] 和 dp[i-1][j-nums[i-1]]。
那么,我们就要做到:在计算新的 dp[j] 的时候,dp[j] 和 dp[j-nums[i-1]] 还是上一轮外层 for 循环的结果。
如果你从前往后遍历一维 dp 数组,dp[j] 显然是没问题的,但是 dp[j-nums[i-1]] 已经不是上一轮外层 for 循环的结果了,这里就会使用错误的状态,当然得不到正确的答案。