# [LeetCode] 39. Combination Sum (Medium) ###### tags: `LeetCode` ## 題目 給定一串排序好的正整數`candidates`,以及一個目標正整數`target`,回傳所有由`candidates`裡面的數組成、加起來為`target`的組合,其中單個數字可以重複出現。 ## 限制條件 * `1 <= candidates.length <= 30` * `1 <= candidates[i] <= 200` * `candidates`中的元素不重複 * `1 <= target <= 500` ## 範例 * Example 1: ``` 輸入: candidates = [2,3,6,7], target = 7 輸出: [[2,2,3],[7]] ``` `2 + 2 + 3 = 7`。注意這邊`2`可以不只出現一次。`7`在`candidates`裡面,而`7 = 7`,所以總共有2種組合。 * Example 2: ``` 輸入: candidates = [2,3,5], target = 8 輸出: [[2,2,2,2],[2,3,3],[3,5]] ``` * Example 3: ``` 輸入: candidates = [2], target = 1 輸出: [] ``` ## 解答 ### 思路 這題可以使用backtracking(回溯)的技巧來解決。聽起來很厲害,但其實只是模仿人腦在解決這種問題的思路而已。 假設現在的`candidates = [3, 4, 5]`,`target = 8`。我們可以把問題轉換成以下形式: > 你身上帶了8塊錢,現在有三種商品,一種3塊錢、一種4塊錢,還有一種5塊錢。每種商品可以重複購買、也可以不購買,那麼有哪些購買方式可以把錢花光? 從空的購物籃開始,可以選擇先放入3塊錢、4塊錢或5塊錢的物品,並接著採取相應的行動: * 如果第一個放入的是3塊錢的物品,第二個放入的物品可以選擇3塊錢、4塊錢或5塊錢的物品。 * 如果第一個放入的是4塊錢的物品,第二個放入的物品可以4塊錢或5塊錢的物品。如果第二個放入的物品是3塊錢的物品,則會和前面考慮的情況重複了。 * 如果第一個放入的是5塊錢的物品,第二個放入的物品只能是5塊錢的物品,理由同上。 按照這樣的邏輯,可以畫出一棵樹來表示可以採取的行動: ![](https://hackmd.io/_uploads/BJkJtR_B5.png) 那麼如果爆掉了,也就是走到圖中的紅色節點,該怎麼辦呢?這時候我們可以沿著原路「回溯」,直到找到其他可行的路為止。 ### 演算法 在這邊我們定義一個遞迴函式`backtrack(remain, comb, start)`來產生題目要求的組合:從`comb`開始,目標是完成`remain`的購買組合,而`start`則是這個購買組合中價格最低的商品在`candidates`的index。 在這裡我們考慮兩個 base case: * `remain = 0`:這時候的組合`comb`加起來剛好滿足`target`,故可以把`comb` append 在`result`上。注意在這邊我們 append 的是`comb.copy()`而不是直接 append `comb`,這是因為如果直接直接 append `comb`的話,後續對`comb`的修改也會影響到`result`。 * `remain < 0`:這時候的組合`comb`加起來超過`target`了,這條路不能再走下去。 接著考慮 recursive case:`remain > 0`。根據前面的討論,我們只需要考慮`candidates[start:]`這個sublist,對裡面的每個元素進行迭代,進行以下操作: * 把目前的元素`candidates[i]` append 到`comb`裡面。 * 因為加入了元素,`remain`減少了`candidates[i]`,所以我們呼叫`backtrack(remain - candidates[i], comb, i)` * 完成每一次迭代時,把最後一個元素 pop 出來以達成backtrack。 這個問題可以看作是一棵 n-ary tree 的尋訪(traversal)問題,尋訪的就是剛剛畫出的那棵樹,而解題的思路事實上就是一個DFS的過程。 ### Code ```python= class Solution: def combinationSum(self, cands: List[int], target: int) -> List[List[int]]: result = [] def backtrack(remain, comb, start): if remain == 0: result.append(comb.copy()) return elif remain < 0: return for i in range(start, len(cands)): comb.append(cands[i]) backtrack(remain - cands[i], comb, i) comb.pop() backtrack(target, [], 0) return result ``` ### 複雜度 假設: * $N$ 為`candidates`的總數 * $T$ 為`target`的值 * $M$ 為`candidates`裡面最小的值 > 時間複雜度:$O\left(N^{\frac{T}{M} + 1}\right)$ 在一個graph上進行DFS的複雜度就是整個graph的vertex數,所以我們現在必須來估計這棵樹上有幾個節點: * 每個節點的分支度為 $O(N)$ * 樹的高度為 $O(T/M + 1)$ 我們知道一棵樹的節點數量可以用$O(分支度^{高度})$ 來估計,因此時間複雜度為 $O\left(N^{\frac{T}{M} + 1}\right)$。 > 空間複雜度:$O\left(\frac{T}{M}\right)$ 在這邊,空間複雜度和遞迴函數呼叫的 stack 大小有關,這個 stack 的大小為 $O\left(\frac{T}{M}\right)$。另一方面,我們需要一個 list of list 來儲存解答需要的組合,大小也是 $O\left(\frac{T}{M}\right)$。所以總共加起來,空間複雜度仍然是 $O\left(\frac{T}{M}\right)$。