动态规划(十九)目标和
问题描述
给你一个正整数数组 nums
和一个整数 target
。现在,可以给 nums
数组中的每个元素添加 +
或 -
,通过一定的添加组合和顺序能够将整个数组 nums
的总和达到 target
,求可行的组合的数量。
例如:对于输入的数组 nums
为 {1,1,1,1,1}
,target
为 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
总共有五种组合方式
取值范围:
0 <= sum(nums[i]) <= 1000
target
的取值范围:-1000 <= target <= 1000
解决思路
-
回溯
首先自然而然地想到通过回溯的方式来解决这个问题,通过不断地在每个位置上添加不同的符号,不断递归进行搜索即可。
递归搜索的结束条件为已经到达
nums
数组的末尾,如果此时的计算结果达到了目标值target
,那么就累加计数器的值 -
动态规划
很明显,使用回溯的方式在计算过程中会重复计算之前已经计算过的值,因此可以使用一个二维数组来存储指定位置索引能够到达的值的数量,从而减少重复计算的次数。
具体地,使用一个二维数组
dp[i][j]
来表示在第i
个位置,这个表达式能够得到目标值j
的数量,由于在每个位置都能够由上一次的位置元素通过添加当前位置的元素或者移除当前位置的元素,因此,dp[i][j]
的转移函数如下所示:\[dp[i][j] = dp[i - 1][j - num[i]] + dp[i - 1][j + nums[i]] \]边界情况为 \(dp[0][0] = 1\),因为在没有任何整数元素的情况下,组合得到目标值为 0 的组合数为 1
值得注意一点是当 \(j < nums[i]\) 的情况,在这种情况下是没有这种发生条件的
因此,最终的转换函数如下所示:
\[dp[i][j] = \begin{cases} 1&i= 0,j = 0\\ 0&i=0,j>0\\ dp[i-1][j+nums[i]]&i>0,j<nums[i]\\ dp[i - 1][j - num[i]] + dp[i - 1][j + nums[i]] & i>0,j>=nums[i] \end{cases} \]最终得到的
dp[n][tagert]
就是最终可行的方案数。 -
优化的动态规划
由于在整个过程中只能添加
+
或者-
,将整个数组的总和定义为 \(sum\),标记为-
符号的总和为 \(neg\),标记为+
的总和则为 \(sum - neg\),目标值为 \(target\)因此,有以下关系:
\[(sum - neg) - neg = sum - 2*neg = target \]简化之后得:
\[neg = \frac{sum-target}{2} \]由于在整个
nums
中只会存在整数,因此 \(neg\) 必定也是一个整数。因此对于 \(sum - target\) 的值不为偶数的情况,是不可能存在这样的组合的。现在问题已经转换为了求组合成目标值 \(target\) 的组合数转换成为了求目标值 \(neg\) 的组合数。
实现
-
回溯
class Solution { int ans = 0; int target; int n; public int findTargetSumWays(int[] nums, int target) { this.target = target; this.n = nums.length; dfs(nums, 0, 0); return ans; } // 递归搜索找到符合条件的目标值 private void dfs(int[] nums, int idx, int sum) { if (idx == n) return; if (idx == n - 1) { if (sum + nums[idx] == target) ans++; if (sum - nums[idx] == target) ans++; return; } dfs(nums, idx + 1, sum + nums[idx]); dfs(nums, idx + 1, sum - nums[idx]); } }
复杂度分析:
时间复杂度:\(O(2^n)\)
空间复杂度:\(O(1)\)
-
动态规划
class Solution { public int findTargetSumWays(int[] nums, int target) { int n = nums.length; /* 由于这里的 target 可能是负的,因此需要进行相应的转换 具体做法比较粗暴,直接将每个目标值加上 1000,以此来抵消负的那一部分 */ int[][] dp = new int[n + 1][3001]; // 边界情况 dp[0][1000] = 1; for (int i = 1; i <= n; ++i) { for (int j = 0; j <= 2000; ++j) { // 与相关的转换函数进行对应 dp[i][j] += dp[i - 1][j + nums[i - 1]]; if (j - nums[i - 1] >= 0) dp[i][j] += dp[i - 1][j - nums[i - 1]]; } } return dp[n][target + 1000]; } }
-
优化的动态规划
class Solution { public int findTargetSumWays(int[] nums, int target) { int N = nums.length; int sum = 0; for (int i = 0; i < N; ++i) sum += nums[i]; int neg = sum - target; if (neg < 0 || neg % 2 != 0) return 0; // 首先判断是否能够得到目标值 neg /= 2; int[] dp = new int[neg + 1]; // 优化空间 dp[0] = 1; for (int num: nums) { for (int j = neg; j >= num; --j) { // 倒序遍历防止重复计数 dp[j] += dp[j - num]; } } return dp[neg]; } }