【Leetcode】刷题之路3(python版)
回溯專題
1.回溯算法的本質(zhì)是n叉樹的深度優(yōu)先搜索,同時,需要注意剪枝減少復(fù)雜度。
2.回溯算法三部曲
- 確定參數(shù)和返回值
- 回溯函數(shù)終止條件
- 單層循環(huán)
3.回溯法思路
回溯法是一種算法思想,而遞歸是一種編程方法,回溯法可以用遞歸來實現(xiàn)。
回溯法的整體思路是:搜索每一條路,每次回溯是對具體的一條路徑而言的。對當(dāng)前搜索路徑下的的未探索區(qū)域進(jìn)行搜索,則可能有兩種情況:
- 當(dāng)前未搜索區(qū)域滿足結(jié)束條件,則保存當(dāng)前路徑并退出當(dāng)前搜索;
- 當(dāng)前未搜索區(qū)域需要繼續(xù)搜索,則遍歷當(dāng)前所有可能的選擇:如果該選擇符合要求,則把當(dāng)前選擇加入當(dāng)前的搜索路徑中,并繼續(xù)搜索新的未探索區(qū)域。
4.回溯算法模版(python版)
在遞歸調(diào)用之前「做選擇」,在遞歸調(diào)用之后「撤銷選擇」
def backtrack(params){if 終止條件:存放結(jié)果;return;}for 遍歷:本層n叉樹的元素:處理節(jié)點;(需要剪枝)backtrack(params,選擇列表);撤銷操作;}
}
回溯法搜所有可行解的模板一般是這樣的:
res = []
path = []def backtrack(未探索區(qū)域, res, path):if 未探索區(qū)域滿足結(jié)束條件:res.add(path) # 深度拷貝returnfor 選擇 in 未探索區(qū)域當(dāng)前可能的選擇:if 當(dāng)前選擇符合要求:path.add(當(dāng)前選擇)backtrack(新的未探索區(qū)域, res, path)path.pop()
重點概括:
- 如果解決一個問題有多個步驟,每一個步驟有多種方法,題目又要我們找出所有的方法,可以使用回溯算法;
- 回溯算法是在一棵樹上的 深度優(yōu)先遍歷(因為要找所有的解,所以需要遍歷),按選優(yōu)條件向前搜索,以達(dá)到目標(biāo),但當(dāng)探索到某一步時,發(fā)現(xiàn)原先選擇并不優(yōu)或達(dá)不到目標(biāo),就退回一步重新選擇,這種走不通就退回再走的技術(shù)為回溯法。回溯算法首先需要畫出遞歸樹,不同的樹決定了不同的代碼實現(xiàn)。
4.回溯算法解決的問題:
- 組合問題:N個數(shù)里面按一定規(guī)則找出k個數(shù)的集合
- 排列問題:N個數(shù)按一定規(guī)則全排列,有幾種排列方式
- 切割問題:一個字符串按一定規(guī)則有幾種切割方式
- 子集問題:一個N個數(shù)的集合里有多少符合條件的子集
- 棋盤問題:N皇后,解數(shù)獨等等
組合問題
- 77. 組合
- 39. 組合總和
- 40. 組合總和II
- 216. 組合總和III
- 17. 電話號碼的字母組合
77. 組合
給定兩個整數(shù) n 和 k,返回范圍 [1, n] 中所有可能的 k 個數(shù)的組合。
你可以按 任何順序 返回答案。
題解
組合問題,相對于排列問題而言,不計較一個組合內(nèi)元素的順序性(即 [1, 2, 3] 與 [1, 3, 2] 認(rèn)為是同一個組合),因此很多時候需要按某種順序展開搜索,這樣才能做到不重不漏。
把組合問題抽象為如下樹形結(jié)構(gòu):
可以看出這個棵樹,一開始集合是 1,2,3,4, 從左向右取數(shù),取過的數(shù),不在重復(fù)取。
根據(jù)三部曲:
(1)遞歸函數(shù)的參數(shù)和返回值
函數(shù)里一定有兩個參數(shù),既然是集合n里面取k的數(shù),那么n和k是兩個int型的參數(shù)。然后還需要一個參數(shù),為int型變量startIndex,這個參數(shù)用來記錄本層遞歸的中,集合從哪里開始遍歷(集合就是[1,…,n] )。
(2)終止條件
path這個數(shù)組的大小如果達(dá)到k,說明我們找到了一個子集大小為k的組合了,在樹中path存的就是根節(jié)點到葉子節(jié)點的路徑。
(3)單層搜索的過程
回溯法的搜索過程就是一個樹型結(jié)構(gòu)的遍歷過程,可以for循環(huán)用來橫向遍歷,遞歸的過程是縱向遍歷。
for循環(huán)每次從startIndex開始遍歷,然后用path保存取到的節(jié)點。
剪枝優(yōu)化:
舉一個🌰,n = 4,k = 4的話,那么第一層for循環(huán)的時候,從元素2開始的遍歷都沒有意義了。 在第二層for循環(huán),從元素3開始的遍歷都沒有意義了。如圖所示:
圖中每一個節(jié)點(圖中為矩形),就代表本層的一個for循環(huán),那么每一層的for循環(huán)從第二個數(shù)開始遍歷的話,都沒有意義,都是無效遍歷。
所以,可以剪枝的地方就在遞歸中每一層的for循環(huán)所選擇的起始位置。如果for循環(huán)選擇的起始位置之后的元素個數(shù) 已經(jīng)不足 我們需要的元素個數(shù)了,那么就沒有必要搜索了
優(yōu)化過程:
接下來看一下優(yōu)化過程如下:
- 已經(jīng)選擇的元素個數(shù):path.size();
- 還需要的元素個數(shù)為: k - path.size();
- 在集合n中至多要從該起始位置 : n - (k - path.size()) + 1,開始遍歷(+1是因為包括起始位置,我們要是一個左閉的集合。)
class Solution:def combine(self, n: int, k: int) -> List[List[int]]:res=[] #存放符合條件結(jié)果的集合path=[] #用來存放符合條件結(jié)果def backtrack(n,k,startIndex):if len(path) == k:res.append(path[:])return for i in range(startIndex,n-(k-len(path))+2): #優(yōu)化的地方path.append(i) #處理節(jié)點 backtrack(n,k,i+1) #遞歸path.pop() #回溯,撤銷處理的節(jié)點backtrack(n,k,1)return res
39. 組合總和
給定一個無重復(fù)元素的正整數(shù)數(shù)組 candidates 和一個正整數(shù) target ,找出 candidates 中所有可以使數(shù)字和為目標(biāo)數(shù) target 的唯一組合。
candidates 中的數(shù)字可以無限制重復(fù)被選取。如果至少一個所選數(shù)字?jǐn)?shù)量不同,則兩種組合是唯一的。
對于給定的輸入,保證和為 target 的唯一組合數(shù)少于 150 個。
題解
思路分析:
根據(jù)示例 1:輸入: candidates = [2, 3, 6, 7],target = 7。
候選數(shù)組里有 2,如果找到了組合總和為 7 - 2 = 5 的所有組合,再在之前加上 2 ,就是 7 的所有組合;
同理考慮 3,如果找到了組合總和為 7 - 3 = 4 的所有組合,再在之前加上 3 ,就是 7 的所有組合,依次這樣找下去。
變量意義:use表示已經(jīng)使用過的數(shù)(組成的列表),remain表示距離target還有多大。
對candidates升序排序,以方便根據(jù)remain的大小使用return減小搜索空間;
遞歸求可能的組合。具體的,每次遞歸時對所有candidates做一次遍歷,情況有三種:
1,滿足條件,則答案加入一條;
2,不足,繼續(xù)遞歸;
3,超出,則直接退出本路線。
注意每層遞歸都對全體candidates做遍歷會導(dǎo)致如[2,2,3],[3,2,2]這樣的對稱重復(fù)的答案的產(chǎn)生。這是因為發(fā)生了 往前選擇 的情況,我們每次更深層的遞歸都往后縮小一個candidates,強(qiáng)制函數(shù)只能 往后選擇 ,這將不會出現(xiàn)重復(fù)答案。
class Solution:def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:candidates = sorted(candidates)ans = []def find(s, use, remain):for i in range(s, len(candidates)):c = candidates[i]if c == remain:ans.append(use + [c])returnif c < remain:find(i, use + [c], remain - c)if c > remain:returnfind(0, [], target)return ans
還有一種標(biāo)準(zhǔn)寫法:
from typing import List# 給定一個無重復(fù)元素的正整數(shù)數(shù)組 candidates 和一個正整數(shù) target ,找出 candidates 中所有可以使數(shù)字和為目標(biāo)數(shù) target 的唯一組合。
# candidates 中的數(shù)字可以無限制重復(fù)被選取。如果至少一個所選數(shù)字?jǐn)?shù)量不同,則兩種組合是唯一的。
# 對于給定的輸入,保證和為 target 的唯一組合數(shù)少于 150 個。class Solution:def combinationSum(candidates: List[int], target: int) -> List[List[int]]:candidates.sort()res=[] #存放符合條件結(jié)果的集合path=[] #用來存放符合條件結(jié)果def backtrack(cur,startIndex):if cur > target: return #剪枝操作if cur == target : return res.append(path[:])for i in range(startIndex,len(candidates)):# if i > startIndex and candidates[i] == candidates[i - 1]:# continuec = candidates[i]path.append(c)backtrack(cur+c,i) #i強(qiáng)制從自己開始往后選擇path.pop() #回溯backtrack(0,0)print(res)return res
40. 組合總和II
給定一個數(shù)組 candidates 和一個目標(biāo)數(shù) target ,找出 candidates 中所有可以使數(shù)字和為 target 的組合。
candidates 中的每個數(shù)字在每個組合中只能使用一次。
注意:解集不能包含重復(fù)的組合。
題解
思路:
和 39. 組合總和 差不多,有以下兩點不同:
1.數(shù)組candidates有重復(fù)數(shù)字;
2.數(shù)組中的數(shù)字不可重復(fù)使用
為了避免產(chǎn)生重復(fù)解,本題candidates務(wù)必排序
backtrack步驟如下:
剪枝:
- 如果當(dāng)前tmp數(shù)組的和cur已經(jīng)大于目標(biāo)target,沒必要枚舉了,直接return
- 如果當(dāng)前tmp數(shù)組的和cur正好和目標(biāo)target相等,找到一個組合,加到結(jié)果res中去,并return
- for循環(huán)遍歷從index開始的數(shù),選一個數(shù)進(jìn)入下一層遞歸。
- 如果從index開始的數(shù)有連續(xù)出現(xiàn)的重復(fù)數(shù)字,跳過該數(shù)字continue,因為這會產(chǎn)生重復(fù)解
- 因為數(shù)不可以重復(fù)選擇,所以在進(jìn)入下一層遞歸時,i要加1,從i之后的數(shù)中選擇接下來的數(shù)
class Solution:def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:res = []n = len(candidates)candidates.sort()def backtrack(tmp, cur, index):if cur > target:returnif cur == target:res.append(tmp)returnfor i in range(index, n):if i > index and candidates[i] == candidates[i - 1]:continuebacktrack(tmp + [candidates[i]], cur + candidates[i], i + 1)backtrack([], 0, 0)return res
標(biāo)準(zhǔn)模版寫法:
class Solution:def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:candidates.sort()res=[] #存放符合條件結(jié)果的集合path=[] #用來存放符合條件結(jié)果def backtrack(cur,startIndex):if cur > target: return #剪枝操作if cur == target : return res.append(path[:])for i in range(startIndex,len(candidates)):if i > startIndex and candidates[i] == candidates[i - 1]:continuec = candidates[i]path.append(c)backtrack(cur+c,i+1)path.pop() #回溯backtrack(0,0)return res
216. 組合總和III
找出所有相加之和為 n 的 k 個數(shù)的組合。組合中只允許含有 1 - 9 的正整數(shù),并且每種組合中不存在重復(fù)的數(shù)字。
說明:
所有數(shù)字都是正整數(shù)。
解集不能包含重復(fù)的組合。
題解
class Solution:def combinationSum3(self, k: int, n: int) -> List[List[int]]:res = [] #存放結(jié)果集path = [] #符合條件的結(jié)果def findallPath(n,k,sum,startIndex):if sum > n: return #剪枝操作if sum == n and len(path) == k: #如果path.size() == k 但sum != n 直接返回return res.append(path[:])for i in range(startIndex,9-(k-len(path))+2): #剪枝操作path.append(i)sum += i findallPath(n,k,sum,i+1) #注意i+1調(diào)整startIndexsum -= i #回溯path.pop() #回溯findallPath(n,k,0,1)return res
17. 電話號碼的字母組合
給定一個僅包含數(shù)字 2-9 的字符串,返回所有它能表示的字母組合。答案可以按 任意順序 返回。
給出數(shù)字到字母的映射如下(與電話按鍵相同)。注意 1 不對應(yīng)任何字母。
示例:
輸入:digits = “23”
輸出:[“ad”,“ae”,“af”,“bd”,“be”,“bf”,“cd”,“ce”,“cf”]
題解
首先使用哈希表存儲每個數(shù)字對應(yīng)的所有可能的字母,然后進(jìn)行回溯操作。
回溯過程中維護(hù)一個字符串,表示已有的字母排列(如果未遍歷完電話號碼的所有數(shù)字,則已有的字母排列是不完整的)。該字符串初始為空。每次取電話號碼的一位數(shù)字,從哈希表中獲得該數(shù)字對應(yīng)的所有可能的字母,并將其中的一個字母插入到已有的字母排列后面,然后繼續(xù)處理電話號碼的后一位數(shù)字,直到處理完電話號碼中的所有數(shù)字,即得到一個完整的字母排列。然后進(jìn)行回退操作,遍歷其余的字母排列。
回溯算法用于尋找所有的可行解,如果發(fā)現(xiàn)一個解不可行,則會舍棄不可行的解。在這道題中,由于每個數(shù)字對應(yīng)的每個字母都可能進(jìn)入字母組合,因此不存在不可行的解,直接窮舉所有的解即可。
class Solution:def letterCombinations(self, digits: str) -> List[str]:if not digits:return list()phoneMap = {"2": "abc","3": "def","4": "ghi","5": "jkl","6": "mno","7": "pqrs","8": "tuv","9": "wxyz",}comb = list()res = list()def backtrack(index: int):if index == len(digits):res.append("".join(comb)) #轉(zhuǎn)化成字符串returnelse:digit = digits[index]for letter in phoneMap[digit]:comb.append(letter)backtrack(index + 1)comb.pop()backtrack(0)return res
一行python代碼,用iterator
class Solution:def letterCombinations(self, digits: str) -> List[str]:if not digits:return list()phoneMap = {"2": "abc","3": "def","4": "ghi","5": "jkl","6": "mno","7": "pqrs","8": "tuv","9": "wxyz",}groups = (phoneMap[digit] for digit in digits)return ["".join(combination) for combination in itertools.product(*groups)]
總結(jié)
以上是生活随笔為你收集整理的【Leetcode】刷题之路3(python版)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 弱精症的检查是什么
- 下一篇: 从头理解self-attention机制