🔗 🟡 494. Target Sum

tags: 陣列(Array) 動態規劃(DP) 回溯(Backtracking)

題意

給定一個 非負 的整數陣列 numsnums 和一個整數 targettarget

你想要利用 numsnums 中的數字來建立一個表達式,方式是在每個數字前面加上符號 ++-,然後把所有數字連接起來。

  • 例如,如果 nums=[2,1]nums = [2, 1],你可以在 22 前加 ++,在 11 前加 -,然後連接起來形成表達式 +21+2-1

返回你可以建立的不同表達式數量,這些表達式的運算結果為 targettarget

約束條件

  • 1nums.length201 \leq nums.length \leq 20
  • 0nums[i]10000 \leq nums[i] \leq 1000
  • 0(nums[i])10000 \leq \sum(nums[i]) \leq 1000
  • 1000target1000-1000 \leq target \leq 1000

思路

這題存在 回溯(Backtracking) 解法,但用 Python 提交會超時,需要使用其他語言提交。

令所有元素的和為 ss,添加符號 ++ 的數字和為 pospos,添加符號 - 的數字和為 negneg,則有:

posneg=(nums)=spos+neg=target\begin{aligned} pos - neg &= \sum(nums) = s\\ pos + neg &= target \end{aligned}

解聯立得 pos=s+target2pos = \frac{s + target}{2} ,但根據約束條件,pospos 必須為整數,且 pos0pos \geq 0,若不滿足則直接返回 00

故問題可以轉換成:在 numsnums 中選取一些數字,使其和為 pospos 的方法數。如此便可以使用 動態規劃(DP) 來解決。

方法一:記憶化搜尋(Memoization)

dfs(i,j)dfs(i, j) 表示在前 ii 個數字中取若干個數字,能夠使和為 jj 的方法數。則有以下遞迴式:

  • dfs(i,j)=dfs(i1,j)+dfs(i1,jnums[i1])dfs(i, j) = dfs(i-1, j) + dfs(i-1, j-nums[i-1])
    • 若選第 ii 個數字 (下標為 i1i-1),需要在剩餘的前 i1i-1 個數字中取若干個數字,使其和為 jnums[i1]j-nums[i-1]
    • 若不選第 ii 個數字,則需要在剩餘的前 i1i-1 個數字中取若干個數字,使其和為 jj
  • 遞迴終點為 i=0i = 0,若 j=0j = 0 代表合法情況,則返回 11;否則返回 00
  • 遞迴入口為 dfs(n,pos)dfs(n, pos) ,其中 nn 為陣列長度,pospos 為目標和,代表在前 nn 個數字中取若干個數字,使其和為 pospos 的方法數。
  • 可以注意到,若 j<nums[i1]j < nums[i-1],則無法選擇第 ii 個數字,則只需要考慮不選的情況即可,可以 剪枝(Pruning)

由於存在大量重複計算,因此可以使用 @cache 裝飾器(Python)或者 memo 陣列(C++)來進行 記憶化(Memoization)

複雜度分析

  • 時間複雜度:O(npos)\mathcal{O}(n \cdot \text{pos}),其中 nn 為陣列長度,pos\text{pos} 為目標和。
    • 總共有 nposn \cdot \text{pos} 個狀態,每個狀態需要 O(1)\mathcal{O}(1) 的時間進行計算。
  • 空間複雜度:O(npos)\mathcal{O}(n \cdot \text{pos}),和狀態數量相同。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
n = len(nums)
pos = target + sum(nums)
if pos % 2 or pos < 0:
return 0
pos //= 2
@cache
def dfs(i: int, j: int) -> int:
if i == 0:
return 1 if j == 0 else 0
if j < nums[i - 1]: # Pruining
return dfs(i - 1, j)
return dfs(i - 1, j) + dfs(i - 1, j - nums[i - 1])
return dfs(n, pos)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int n = nums.size();
target += accumulate(nums.begin(), nums.end(), 0);
if (target % 2 || target < 0) return 0;
target /= 2;
vector<vector<int>> memo(n + 1, vector<int>(target + 1, -1));
function<int(int, int)> dfs = [&](int i, int j) -> int {
if (i == 0) return j == 0;
if (memo[i][j] != -1) return memo[i][j];
if (j < nums[i - 1]) return dfs(i - 1, j); // Pruning
return memo[i][j] = dfs(i - 1, j) + dfs(i - 1, j - nums[i - 1]);
};
return dfs(n, target);
}
};

方法二:使用滾動陣列(Scorelling Array)優化空間

注意到在方法一中,狀態的轉移都只和 i1i-1 的狀態有關,因此可以使用 滾動陣列(Scorelling Array) 的方式進行空間優化。

以同樣的方式計算出 pospos,然後定義一個陣列 dpdp,其中 dp[j]dp[j] 表示在前 ii 個數字中取若干個數字,使其和為 jj 的方法數。由於我們只要考慮 i1i-1 的狀態,因此只需要保存一個長度為 pospos 的陣列即可。

  • 初始化 dp[0]=1dp[0] = 1,表示不選取任何數字,結果為 00 的表達式有 11 種方法。
  • 遍歷 numsnums 中的每個數字 xx,對於每個 j[x,pos]j \in [x, pos],更新 dp[j]+=dp[jx]dp[j] += dp[j - x]
    • 這裡的 dp[jx]dp[j - x] 表示在前 i1i-1 個數字中取若干個數字,使其和為 jxj-x 的方法數,因此可以選擇加上 xx 來形成和為 jj 的情況。
    • 為了避免計算時使用到已經被修改的值,我們需要 由大到小 遍歷 jj
  • 最後答案為 dp[pos]dp[pos],即前 nn 個數字中組成和為 pospos 的方法數。

複雜度分析

  • 時間複雜度:O(npos)\mathcal{O}(n \cdot \text{pos}),其中 nn 為陣列長度,pos\text{pos} 為目標和。
    • 總共有 nposn \cdot \text{pos} 個狀態,每個狀態需要 O(1)\mathcal{O}(1) 的時間進行計算。
  • 空間複雜度:O(pos)\mathcal{O}(\text{pos}),只保存一維的狀態陣列 dpdp
1
2
3
4
5
6
7
8
9
10
11
12
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
pos = target + sum(nums)
if pos % 2 or pos < 0:
return 0
k = pos // 2
dp = [0] * (k + 1)
dp[0] = 1
for x in nums:
for j in range(k, x - 1, -1):
dp[j] += dp[j - x]
return dp[k]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int n = nums.size();
target += accumulate(nums.begin(), nums.end(), 0);
if (target % 2 || target < 0) return 0;
target /= 2;
vector<int> dp(target + 1);
dp[0] = 1;
for (int i = 0; i < n; i++)
for (int j = target; j >= nums[i]; j--)
dp[j] += dp[j - nums[i]];
return dp[target];
}
};

寫在最後

Cover photo is generated by @たろたろ, thanks for their work!