動態規劃(十九)目標和
問題描述
給你一個正整數陣列 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[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]; } }