回溯問題Python框架總結——排列組合問題

本文是對leetcode回溯題的一些模板進行整理總結,不少關於回溯的blog都會引用對回溯算法的official definition和通用的解題步驟,若是是真的想研究這一算法思想,按照這樣的方式來徹底沒有問題。不過我的以爲若是僅僅只是爲了應試,那麼掌握一些解題的模板會更直接的幫助理解回溯的算法思想。本文將舉一些簡單的例子來講明這些模板,不採用樹來描述,使得對於數據結構不太瞭解的讀者也相對友好。html

基本思想:

回溯問題是對多叉樹的深度搜索,遇到不知足條件的節點則回退,遞歸的搜索答案。在遞歸調用前,嘗試一種可能的方案,那麼在遞歸調用的時候,函數的開始,有判斷語句,若是這種方案可行,記錄下這種方案,而且return,不然,繼續進行嘗試,找到知足條件的解之後,回退到以前的選擇。git

常見模板:

一、全排列問題

通常在回溯的過程當中,不斷縮小原來數組的範圍並添加至track中,直至枚舉完全部的元素,知足條件的添加到result數組中, 模板以下算法

 1 def problem(nums):
 2     res = []
 3     def backtrack(nums, track):
 4         if (判斷知足題目所給的條件):
 5             res.append(track[:]) #這裏必須傳入track的拷貝,track[:], 不然答案全是空
 6             return
 7         for i in range(len(nums)):
 8             backtrack(nums[:i] + nums[i+1:], track + nums[i])
 9     backtrack(nums, [])
10     return 題目須要的res相關的參數,輸出自己,長度,或者其餘的

 

如下兩題爲實戰中套用框架解題數組

Leetcode 46  全排列數據結構

因爲是全排列,只要沒得選了,那就是咱們所需的答案,加入result而且returnapp

 1 class Solution:
 2     def permute(self, nums: List[int]) -> List[List[int]]:
 3         res = []
 4         def backtrack(nums, track):
 5             if not nums:
 6                 res.append(track[:])
 7                 return
 8             for i in range(len(nums)):
 9                 backtrack(nums[:i] + nums[i+1:], track+[nums[i]])
10         backtrack(nums, [])
11         return res

 

Leetcode 1079  活字印刷框架

依舊是一個全排列的問題,差異僅僅在於,此次沒有限制須要全部的字符都要用到,而是任意長度符合條件都可。惟一的問題在於須要去掉重複的排列,直接使用集合判斷是否有重複會很方便,相比於在res數組中用if xxx not in 要有顯著的效率的提升。函數

最終結果須要去掉一個空的排列,由於題目要求最後的結果非空。spa

1 class Solution:
2     def numTilePossibilities(self, tiles: str) -> int:
3         res = set()  #使用集合去重
4         def backtrack(tiles, track):
5             res.add(track)
6             for i in range(len(tiles)):
7                 backtrack(tiles[:i] + tiles[i+1:], track + tiles[i])
8         backtrack(tiles, "")
9         return len(res) - 1

二、數組元素重複且數組元素能夠重複使用的組合問題

這種問題在高中找多少種不一樣的組合比較常見,好比找 [1,2,3] 這樣的數組有多少種非空的子集,那麼咱們按照高中的不重複不遺漏的找法,通常是先肯定1,而後找2,3裏面的,第一輪找出來是 [1], [1,2], [1,3], [1,2,3],這時候對於1來講,沒有更多的元素能夠和它組成子集了,那麼如今去掉1,再從 [2,3]裏面找剩餘的,第二輪出來的是 [2], [2,3],最後一輪從 [3] 中找,也就是 [3]。這樣咱們就獲得了不重複不遺漏的全部非空子集。code

能夠看到,這種問題,越搜索,數據範圍越小,比上一輪起始數據向後移動了一位,那麼在遞歸調用中就能夠用一個index標誌+1來表示如今的起始位置從上一輪+1的位置開始。框架以下

 1 def problem(nums):
 2     res = []
 3     def backtrack(index, track):
 4         if (知足題目中的條件):
 5             res.append(track[:])
 6             return
 7         for i in range(index, len(nums)):
 8             backtrack(i + 1, track + [nums[i]])
 9     backtrack(0, []) #這裏不必定是0,根據實際的起始條件來給
10     return res

 

如下三題爲實戰中用框架解題

Leetcode 77  組合

實際問題的返回條件是每一個組合內有k個數,那麼就是track長度須要是k的時候返回。因爲這裏題目並無直接給出數組,是用1-n來代替,那麼起始條件就是1,數組用1-n的範圍來代替就好。

 1 class Solution:
 2     def combine(self, n: int, k: int) -> List[List[int]]:
 3         res = []
 4         def backtrack(index, track):
 5             if len(track) == k:
 6                 res.append(track[:])
 7                 return
 8             for i in range(index, n+1):
 9                 backtrack(i + 1, track + [i])
10         backtrack(1, [])
11         return res

 

Leetcode 78  子集

直接套入框架,這裏每一次搜索的路徑都要記錄下來,那就記錄一下每次的路徑就好了,不須要再判斷何時的結果才保存

1 class Solution:
2     def subsets(self, nums: List[int]) -> List[List[int]]:
3         res = []
4         def backtrack(index, track):
5             res.append(track[:])
6             for i in range(index, len(nums)):
7                 backtrack(i+1, track + [nums[i]])
8         backtrack(0, [])
9         return res

 

Leetcode 17  電話號碼中的字母組合

此題看上去數組中的數能夠重複,好比能夠撥打「232」,可是因爲是字符串,順序是必定的,並且撥打第一個2和第二個2,對應的字母也可能不一樣,因此仍然能夠看作是數組中元素不重複且不能重複使用的問題。

用字典記錄下對應關係,以後代入框架便可,注意讀取字典鍵和值的各類括號就行,最終結果是字符串的時候,track初始設爲「」替代[]

 1 class Solution:
 2     def letterCombinations(self, digits: str) -> List[str]:
 3         if not digits:
 4             return []
 5         res = []
 6         dic = {'2':'abc','3':'def','4':'ghi','5':'jkl','6':'mno','7':'pqrs','8':'tuv','9':'wxyz'}
 7         def backtrack(index, track):
 8             if len(track) == len(digits):
 9                 res.append(track)
10                 return
11             for i in range(len(dic[digits[index]])):
12                 backtrack(index + 1, track + dic[digits[index]][i])
13         backtrack(0, "")
14         return res

三、數組元素重複但能夠重複使用的組合問題

這一類問題和第二種類型的問題類似,最主要的是要對結果進行去重,也就是對深搜的N叉樹進行剪枝。好比咱們要找 [2,1,2,4] 有多少種不重複的子集組合,按照咱們的高中知識,爲了避免重複不遺漏,咱們應該先排序這個數組,獲得[1,2,2,4],這時候從1開始找,第一輪是 [1], [1,2],接下來遇到一個相同的2,咱們爲了避免重複,會跳過它,不看,由於 len = 2 的時候,若是再選2,就會獲得重複的結果,而後是 [1,4], [1, 2, 2], [1, 2, 4], [1,2,2,4],咱們在找 len=3的時候,一樣,當第二位選了第一個2之後,第二位就再也不考慮選第二個2的狀況,由於它們的結果相同,至此,第一輪結束。

第二輪去掉1,在[2,2,4]裏面找,[2],  [2,2], [2,4], [2,2,4], 第三輪去掉一個2,原本應該在[2,4]裏面找,假如咱們這樣找結果,會獲得 [2], [2,4],產生重複,由於 [2,4] 的狀況已經包含在 [2,2,4] 中了,這就是有重複元素的狀況下,咱們在同一個位置進行選擇的時候,應該跳過相同的元素,不然會產生重複。第三輪實際在 [4] 裏面找,獲得 [4]。

框架以下

 1 def problem(nums):
 2     res = []
 3     nums.sort() #排序,爲了後面去重作準備
 4     def backtrack(index, track):
 5         if (知足題目條件):
 6             res.append(track[:])
 7             for i in range(index, len(nums)):
 8                 ###進行剪枝,跳過相同位置重複的數字選擇
 9                 if i > index and nums[i] == nums[i-1]: 
10                     continue
11                 backtrack(i + 1, track + [nums[i]])
12     backtrack(0, [])
13 return res 

 

如下兩題爲實戰中用框架解題

Leetcode 90  子集2

搜索路徑上全部結果所有保留,直接套入上述框架便可

 1 class Solution:
 2     def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
 3         res = []
 4         nums.sort()
 5         def backtrack(index, track):
 6             res.append(track[:])
 7             for i in range(index, len(nums)):
 8                 if i > index and nums[i] == nums[i-1]:
 9                     continue
10                 backtrack(i + 1, track + [nums[i]])
11         backtrack(0, [])
12         return res

 

Leetcode 40  組合總和2

這裏惟一的差異是在於須要把目標和也一塊兒代入進遞歸調用中,每次判斷若是是目標和就加入最終結果,加超過了目標和那就不符合,直接跳出

 1 class Solution:
 2     def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
 3         candidates.sort()
 4         res = []
 5         def backtrack(index, track, target):
 6             if target == 0:
 7                 res.append(track[:])
 8                 return
 9             for i in range(index, len(candidates)):
10                 if target - candidates[i] < 0: # 超過目標和
11                     break
12                 if i > index and candidates[i] == candidates[i-1]:
13                     continue
14                 backtrack(i + 1, track + [candidates[i]], target - candidates[i])
15         backtrack(0, [], target)
16         return res

四、數組元素重複但能夠重複使用

這一類的問題一樣也是第二種問題演變而來,惟一的區別是遞歸調用backtrack的時候,把 i + 1 改爲 i ,那麼下一個位置又能夠用這個元素了,便可實現有重複

Leetcode 39  組合總和

 1 class Solution:
 2     def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
 3         res = []
 4         candidates.sort()
 5         def backtrack(index, track, target):
 6             if target == 0:
 7                 res.append(track[:])
 8                 return
 9             for i in range(index, len(candidates)): 
10                 if target - candidates[i] < 0:
11                     break
12                 ###把原來遞歸的時候 i+1 改爲 i,當前的元素又能夠再用一次了
13                 backtrack(i, track + [candidates[i]], target - candidates[i])
14         backtrack(0, [], target)
15         return res
相關文章
相關標籤/搜索