leetcode刷題筆記——回溯法
leetcode刷題筆記——回溯法
目前完成的回溯法的leetcode演算法題序號:
中等:60, 93
困難:
來源:力扣(LeetCode)
連結:https://leetcode-cn.com/problems/reconstruct-itinerary
著作權歸領釦網路所有。商業轉載請聯絡官方授權,非商業轉載請註明出處。
文章目錄
演算法理解
回溯法 採用試錯的思想,它嘗試分步的去解決一個問題。在分步解決問題的過程中,當它通過嘗試發現現有的分步答案不能得到有效的正確的解答的時候,它將取消上一步甚至是上幾步的計算,再通過其它的可能的分步解答再次嘗試尋找問題的答案。回溯法通常用最簡單的遞迴方法來實現,在反覆重複上述的步驟後可能出現兩種情況:
找到一個可能存在的正確的答案;
在嘗試了所有可能的分步方法後宣告該問題沒有答案。
做題的時候,建議 先畫樹形圖 ,畫圖能幫助我們想清楚遞迴結構,想清楚如何剪枝。拿題目中的示例,想一想人是怎麼做的,一般這樣下來,這棵遞迴樹都不難畫出。
在畫圖的過程中思考清楚:
- 分支如何產生;
- 題目需要的解在哪裡?是在葉子結點、還是在非葉子結點、還是在從跟結點到葉子結點的路徑?
- 哪些搜尋會產生不需要的解的?例如:產生重複是什麼原因,如果在淺層就知道這個分支不能產生需要的結果,應該提前剪枝,剪枝的條件是什麼,程式碼怎麼寫?
一、60題:排列序列
1.題幹
給出集合 [1,2,3,…,n],其所有元素共有 n! 種排列。
按大小順序列出所有排列情況,並一一標記,當 n = 3 時, 所有排列如下:
“123”
“132”
“213”
“231”
“312”
“321”
給定 n 和 k,返回第 k 個排列。
2.思路
最直接的思路是通過回溯法得到所有的排列,然後從取出對應的排列。
但是這題顯然考察的是回溯法的剪枝,如何高效的剪枝才是關鍵。
根據題意,要尋找第k個排列,就需要考慮按照哪種排列方式下的第k種,根據例子可以看出按照大小順序得到的排列集合,與常規的回溯法得到的集合是一樣的順序,我們就只需要考慮剪枝的問題。
這裡第k個排列,可以發現,排列的第一個數有n種選擇,第一個數確定之後,後續的 n − 1 n-1 n−1個數有 ( n − 1 ) ! (n-1)! (n−1)!種組合,所以通過 k / ( n − 1 ) ! k/(n-1)! k/(n−1)!就能得到目標排列的第一個數應該是哪個,後續的依次類推,要注意的是這裡第k個排列,k是從1開始的,這與索引常用的0開始,要區別開來。
3.程式碼
import math
class Solution:
def getPermutation(self, n: int, k: int) -> str:
def dfs(n, level, used, path, k):
if len(path) == n:
return
#m1表示這一層應該選擇第幾個數進入排列
m1 = k // math.factorial(level-1)
#m2表示經過這一層處理之後,進入下一層還需要第幾個數進入排列,用於下一次遞迴
m2 = k % math.factorial(level-1)
tmp = 0
#這裡根據m1確定了需要第幾個數進入佇列之後,就沒必要對前面的數進行迴圈,因此wuile更加合適,用來直接尋找列表中未使用過的第幾個數
while m1 >= 0:
if used[tmp] == False:
m1 -= 1
tmp += 1
else:
tmp += 1
continue
path.append(tmp)
used[tmp-1] = True
dfs(n, level-1, used, path, m2)
path = []
used = [False for _ in range(n)]
#注意:這裡開始遞迴時,將k統一到從0開始
dfs(n, n, used, path, k-1)
return "".join([str(x) for x in path])
二、93題:復原IP地址
1.題幹
給定一個只包含數字的字串,復原它並返回所有可能的 IP 地址格式。
有效的 IP 地址 正好由四個整數(每個整數位於 0 到 255 之間組成,且不能含有前導 0),整數之間用 ‘.’ 分隔。
例如:“0.1.2.201” 和 “192.168.1.1” 是 有效的 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “[email protected]” 是 無效的 IP 地址。
2.解題思路
這裡需要將問題進行轉化,問題的核心是:
1)對一個數組字串進行拆解,得到四個長度為3的字串;
2)且每個字串的數值大小不能大於255;
3)且每個字串不能有前導0;
本題可以轉化為:四數之和等於初始字串長度n,只不過需要根據相關要求,確定各個剪枝條件
3.程式碼
class Solution:
def restoreIpAddresses(self, s: str) -> List[str]:
n = len(s)
def dfs(path, res, rest, alr):
'''
path:字串切分的長度,組成的列表,目標長度是4;
res:用於儲存符合條件的結果(path);
rest:表示初始字串經過前面的切分之後還剩下的字元的個數;
alr:表示當前已經切分出來了幾個字串;
'''
#如果已經切分出了四個字串,且初始字串中沒有未切分的字元之後,該切分組合即為滿足條件的結果,否則是無效的結果,直接放棄;
if alr == 4:
if rest == 0:
res.append(path)
return
else:
return
#每個切分字串的長度最大為3
for i in range(1, 4):
#剪枝1:如果切分的長度>初始字串的剩餘長度,無效,放棄這種可能
#剪枝2:如果剩下的切分次數中,即使每次長度最大也無法分完剩下的字元,無效,放棄這種可能
if i > rest or (4-alr)*3 < rest:
break
#剪枝3:如果切分的字串的長度大於1,且是以0開頭,無效,放棄這種可能
if i > 1 and s[-1*rest] == "0":
break
#剪枝4:每次切分的字串的數值不能大於255
#注意:這裡當rest<=3的時候,由於切片索引取不到列表的最後一個值而取不到目標字串,會引發報錯
#這裡通過中間變數tmp,如果rest=3,且切分的大小為3,則直接取剩餘二點所有元素
tmp = s[-1*rest:-1*rest+3] if rest > 3 else s[-1*rest:]
if i == 3 and int(tmp) > 255:
continue
dfs(path+[i], res, rest-i, alr+1)
res = []
path = []
output = []
alr = 0
dfs(path, res, n, alr)
#根據字串切分的長度,得到各種字串切分結果
for l in res:
for i in range(1,4):
l[i] = l[i] + l[i-1]
for l in res:
output.append(s[:l[0]]+'.'+s[l[0]:l[1]]+'.'+s[l[1]:l[2]]+'.'+s[l[2]:l[3]])
return output
4. 注意點
剪枝條件,需要細緻,並多次除錯
四、
1. 題幹
2. 解題思路
3. 程式碼