前言:回溯法和動態規劃應該是考察率最多的兩類算法,參考《從「排列問題」理解「回溯搜索」(DFS + 狀態重置)、位掩碼技巧、遞歸交換》,作一次總結css
力扣No. 46 全排列java
解決回溯問題,個人經驗是 必定不要偷懶,拿起紙和筆,把這個問題的遞歸結構畫出來,通常而言,是一個樹形結構,這樣思路和代碼就會比較清晰了。而寫代碼便是將畫出的圖用代碼表現出來。python
以示例輸入: [1, 2, 3]
爲例,由於是排列問題,只要咱們按照順序選取數字,保證上層選過的數字不在下一層出現,就可以獲得不重不漏的全部排列。git
畫出樹形結構以下圖程序員
注意:
一、這裏特別說明一點:雖然個人圖是一會兒展現出來的,可是我想你畫出的圖應該是一層一層畫出來的;
二、在每一層,咱們都有若干條分支供咱們選擇。下一層的分支數比上一層少 1 ,由於每一層都會排定 1 個數,從這個角度,再來理解一下爲何要使用額外空間記錄那些元素使用過;
三、所有的「排列」正是在這棵遞歸樹的全部葉子結點。算法
咱們把上面這件事情給一個形式化的描述:問題的解空間是一棵遞歸樹,求解的過程正是在這棵遞歸樹上搜索答案,而搜索的路徑是「深度優先遍歷」,它的特色是「不撞南牆不回頭」。數組
在程序執行到上面這棵樹的葉子結點的時候,此時遞歸到底,當前根結點到葉子結點走過的路徑就構成一個全排列,把它加入結果集,我把這一步稱之爲「結算」。網絡
此時遞歸方法要返回了,對於方法返回之後,要作兩件事情:數據結構
(1)釋放對最後一個數的佔用;
(2)將最後一個數從當前選取的排列中彈出。app
事實上在每一層的方法執行完畢,即將要返回的時候都須要這麼作。這棵樹上的每個結點都會被訪問 2 次,繞一圈回到第 1 次來到的那個結點,第 2 次回到結點的「狀態」要和第 1 次來到這個結點時候的「狀態」相同,這種程序員賦予程序的操做叫作「狀態重置」。
狀態重置」是「回溯」的重要操做,「回溯搜索」是有方向的搜索,不然咱們要寫多重循環,代碼量不可控。
說明:
一、數組 used 記錄了索引 i 在遞歸過程當中是否被使用過,還能夠用哈希表、位圖來代替,在下面的參考代碼 2 和參考代碼 3 分別提供了 Java 的代碼;
二、當程序第 1 次走到一個結點的時候,表示考慮一個數,要把它加入列表,通過更深層的遞歸又回到這個結點的時候,須要「狀態重置」、「恢復現場」,須要把以前考慮的那個數從末尾彈出,這都是在一個列表的末尾操做,最合適的數據結構是棧(Stack)。
class Solution: def permute(self, nums): if len(nums) == 0: return [] used = [False] * len(nums) res = [] self.__dfs(nums, 0, [], used, res) return res def __dfs(self, nums, index, pre, used, res): # 先寫遞歸終止條件 if index == len(nums): res.append(pre.copy()) return for i in range(len(nums)): if not used[i]: # 若是沒有用過,就用它 used[i] = True pre.append(nums[i]) # 在 dfs 先後,代碼是對稱的 self.__dfs(nums, index + 1, pre, used, res) used[i] = False pre.pop()
總結:
能夠經過這個例子理解「回溯」算法的「狀態重置」的操做,「回溯搜索」 = 「深度優先遍歷 + 狀態重置 + 剪枝」。
一、「深度優先遍歷」 就是「不撞南牆不回頭」;
二、回頭的時候要「狀態重置」,即回到上一次來到的那個地方,「狀態」要和上一次來的時候同樣。
三、在代碼上,每每是在執行下一層遞歸的先後,代碼的形式是「對稱的」。
注意:
ask: 爲何 代碼 res.append(pre.copy()):
liweiwei14199 : 這是由於在 Python、Java 語言中「可變對象」在「方法傳參」中傳遞的是引用,若是使用 res.append(pre)
的話,在 res
變量裏存的就是在葉子結點處 pre
變量的引用,而 pre
變量在最終回溯完成之後是空列表 []
,使用 res.append(pre)
您會看到一堆空列表。
所以,須要使用 pre[:]
切片操做,獲得一個拷貝,或者使用 pre.copy()
方法。
這部分的知識您能夠在網絡中搜索「Python 傳參機制」、「值傳遞」、「引用傳遞」、「深拷貝」、「淺拷貝」、「列表的拷貝和切片」等關鍵字得到相關的知識。解釋清楚這些知識已經在個人能力範圍以外了。
若是我有解釋錯誤的地方,歡迎您指出。
做者:liweiwei1419
連接:https://leetcode-cn.com/problems/permutations/solution/hui-su-suan-fa-python-dai-ma-java-dai-ma-by-liweiw/
來源:力扣(LeetCode)
著做權歸做者全部。商業轉載請聯繫做者得到受權,非商業轉載請註明出處。
力扣No.17 電話號碼的字母組合
class Solution: def letterCombinations(self, digits: str) -> List[str]: if not digits: return [] dict1 = {'2':'abc', '3':'def', '4':'ghi', '5':'jkl', '6':'mno', '7':'pqrs', '8':'tuv', "9":'wxyz'} paths = [] self.__dfs(paths, [], digits, 0, dict1) return paths def __dfs(self, paths, path, digits, index, dict1): if index == len(digits): paths.append(''.join(path.copy())) return for i in range(len(dict1[digits[index]])): path.append(dict1[digits[index]][i]) self.__dfs(paths, path, digits, index+1, dict1) path.pop()
注意:
path 列表中數據爲 ['a', 'e', 'i', ...]
因此須要用: paths.append(''.join(path.copy()))
path.copy() 千萬不能忘!!!