【Leetcode】刷题之路4(python版)
接上章回溯專題,本章挑選了分割問題、子集問題、排列問題。
- 分割問題
- 131.分割回文串
- 93.復(fù)原IP地址
- 子集問題
- 78.子集
- 90.子集II
- 排列問題
- 46.全排列
- 47.全排列II
分割問題
我們來分析一下切割,其實切割問題類似組合問題。
例如對于字符串a(chǎn)bcdef:
- 組合問題:選取一個a之后,在bcdef中再去選取第二個,選取b之后在cdef中在選組第三個…
- 切割問題:切割一個a之后,在bcdef中再去切割第二段,切割b之后在cdef中在切割第三段…
所以切割問題,也可以抽象為一顆樹形結(jié)構(gòu),如下所示:
(圖片來源鏈接:leetcode)
遞歸用來縱向遍歷,for循環(huán)用來橫向遍歷,切割線(就是圖中的紅線)切割到字符串的結(jié)尾位置,說明找到了一個切割方法。
此時可以發(fā)現(xiàn),切割問題的回溯搜索的過程和組合問題的回溯搜索的過程是差不多的。
回溯法搜所有可行解的模板一般是這樣的:
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()
131. 分割回文串
給你一個字符串 s,請你將 s 分割成一些子串,使每個子串都是 回文串 。返回 s 所有可能的分割方案。
回文串 是正著讀和反著讀都一樣的字符串。
示例:
輸入:s = “aab”
輸出:[[“a”,“a”,“b”],[“aa”,“b”]]
解題思路
看到題目要求「所有可能的結(jié)果」,而不是「結(jié)果的個數(shù)」,一般情況下,我們就知道需要暴力搜索所有的可行解了,可以用「回溯法」。
「回溯法」實際上一個類似枚舉的搜索嘗試過程,主要是在搜索嘗試過程中尋找問題的解,當(dāng)發(fā)現(xiàn)已不滿足求解條件時,就「回溯」返回,嘗試別的路徑。
在每次搜索位置區(qū)域的時候,使用的是產(chǎn)生一個新數(shù)組 path + [s[:i]] ,這樣好處是方便:不同的路徑使用的是不同的 path,因此不需要 path.pop() 操作;而且 res.append(path) 的時候不用深度拷貝一遍 path。
class Solution(object):def partition(self, s):self.isPalindrome = lambda s : s == s[::-1] #判斷是否時回文字符串res = []self.backtrack(s, res, [])return resdef backtrack(self, s, res, path):if not s: #判斷是否str變量為空res.append(path)returnfor i in range(1, len(s) + 1): #注意起始和結(jié)束位置if self.isPalindrome(s[:i]):self.backtrack(s[i:], res, path + [s[:i]])#path+[s[:i]] can only concate list to list
時間復(fù)雜度:O(N * 2 ^ N),因為總共有 O(2^N)種分割方法,每次分割都要判斷是否回文需要 O(N)O(N) 的時間復(fù)雜度。
空間復(fù)雜度:O(N ^ 2),這里不計算返回答案占用的空間的話,數(shù)組 f 需要使用的空間為 O(n ^ 2),而在回溯的過程中,我們需要使用 O(n) 的棧空間以及 O(n) 的用來存儲當(dāng)前字符串分割方法的空間。
93. 復(fù)原IP地址
有效 IP 地址 正好由四個整數(shù)(每個整數(shù)位于 0 到 255 之間組成,且不能含有前導(dǎo) 0),整數(shù)之間用 ‘.’ 分隔。
- 例如:“0.1.2.201” 和 “192.168.1.1” 是 有效 IP 地址,但是 “0.011.255.245”、“192.168.1.312” 和 “192.168@1.1” 是 無效 IP 地址。
給定一個只包含數(shù)字的字符串 s ,用以表示一個 IP 地址,返回所有可能的有效 IP 地址,這些地址可以通過在 s 中插入 ‘.’ 來形成。你 不能 重新排序或刪除 s 中的任何數(shù)字。你可以按 任何 順序返回答案。
示例 1:
輸入:s = “25525511135”
輸出:[“255.255.11.135”,“255.255.111.35”]
示例 2:
輸入:s = “0000”
輸出:[“0.0.0.0”]
示例 3:
輸入:s = “101023”
輸出:[“1.0.10.23”,“1.0.102.3”,“10.1.0.23”,“10.10.2.3”,“101.0.2.3”]
解題思路
只要意識到這是切割問題,切割問題就可以使用回溯搜索法把所有可能性搜出來,和剛做過的131.分割回文串就十分類似了。
切割問題可以抽象為樹型結(jié)構(gòu)。
切割之后,判斷子串是否合法,主要考慮到如下三點:
- 段位以0為開頭的數(shù)字不合法
- 段位里有非正整數(shù)字符不合法
- 段位如果大于255了不合法
回溯三部曲:
-
遞歸參數(shù)
切割問題類似于組合問題。
因為不能重復(fù)分割,記錄下一層遞歸分割的起始位置startIndex。
還需要一個變量pointNum,記錄添加逗點的數(shù)量。 -
遞歸終止條件
終止條件和131.分割回文串情況就不同了,本題明確要求只會分成4段作為終止條件,逗點數(shù)量為3說明字符串分成了4段。
然后驗證一下第四段是否合法,如果合法就加入到結(jié)果集里。 -
單層搜索邏輯
在 for 循環(huán)中,[startIndex, i]這個區(qū)間就是截取的子串,需要判斷這個子串是否合法。如果合法就在字符串后面加上符號.表示已經(jīng)分割;如果不合法就結(jié)束本層循環(huán),就剪掉該分支。
然后就是遞歸和回溯的過程:遞歸調(diào)用時,下一層遞歸的startIndex要從i+2開始(因為需要在字符串中加入了分隔符.),同時記錄分割符的數(shù)量pointNum 要 +1。回溯的時候,就將剛剛加入的分隔符. 刪掉就可以了,pointNum也要-1。
class Solution:def restoreIpAddresses(self, s: str) -> List[str]:res = []path = [] # 存放分割后的字符# 判斷數(shù)組中的數(shù)字是否合法def isValid(p):if p == '0': return True # 解決"0000"if p[0] == '0': return Falseif int(p) > 0 and int(p) <256: return Truereturn Falsedef backtrack(s, startIndex):if len(s) > 12: return # 字符串長度最大為12if len(path) == 4 and startIndex == len(s): # 確保切割完,且切割后的長度為4res.append(".".join(path[:])) # 字符拼接returnfor i in range(startIndex, len(s)):if len(s) - startIndex > 3*(4 - len(path)): continue # 剪枝,剩下的字符串大于允許的最大長度則跳過p = s[startIndex:i+1] # 分割字符if isValid(p): # 判斷字符是否有效path.append(p)else: continuebacktrack(s, i + 1) # 尋找i+1為起始位置的子串path.pop()backtrack(s, 0)return res
子集問題
如果把 子集問題、組合問題、分割問題 都抽象為一棵樹的話,那么組合問題和分割問題都是收集樹的葉子節(jié)點,而子集問題是找樹的所有節(jié)點!
其實子集也是一種組合問題,因為它的集合是無序的,子集{1,2} 和 子集{2,1}是一樣的。既然是無序,取過的元素不會重復(fù)取,寫回溯算法的時候,for就要從startIndex開始,而不是從0開始!
什么時候for可以從0開始呢?
求排列問題的時候,就要從0開始,因為集合是有序的,{1, 2} 和{2, 1}是兩個集合,排列問題我們后續(xù)的文章就會講到的。
以示例中nums = [1,2,3]為例把求子集抽象為樹型結(jié)構(gòu),如下:
(圖片來源leetcode代碼隨想錄)
從圖中紅線部分,可以看出遍歷這個樹的時候,把所有節(jié)點都記錄下來,就是要求的子集集合。
78. 子集
給你一個整數(shù)數(shù)組 nums ,數(shù)組中的元素 互不相同 。返回該數(shù)組所有可能的子集(冪集)。
解集 不能 包含重復(fù)的子集。你可以按 任意順序 返回解集。
輸入:nums = [1,2,3]
輸出:[[],[1],[2],[1,2],[3],[1,3],[2,3],[1,2,3]]
解題思路
求取子集問題,不需要任何剪枝!因為子集就是要遍歷整棵樹
class Solution:def subsets(self, nums: List[int]) -> List[List[int]]:res = [] path = [] def backtrack(nums,startIndex):res.append(path[:]) #收集子集,要放在終止添加的上面,否則會漏掉自己for i in range(startIndex,len(nums)): #當(dāng)startIndex已經(jīng)大于數(shù)組的長度了,就終止了,for循環(huán)本來也結(jié)束了,所以不需要終止條件path.append(nums[i])backtrack(nums,i+1) #遞歸,每次遞歸的下一層就是從i+1開始的path.pop() #回溯backtrack(nums,0)return res
90. 子集II
給你一個整數(shù)數(shù)組 nums ,其中可能包含重復(fù)元素,請你返回該數(shù)組所有可能的子集(冪集)。
解集 不能 包含重復(fù)的子集。返回的解集中,子集可以按 任意順序 排列。
輸入:nums = [1,2,2]
輸出:[[],[1],[1,2],[1,2,2],[2],[2,2]]
解題思路
和78.子集問題中不同的是 怎么去重?
這個去重為什么很難理解呢,所謂去重,其實就是使用過的元素不能重復(fù)選取。 這么一說好像很簡單!
都知道組合問題可以抽象為樹形結(jié)構(gòu),那么“使用過”在這個樹形結(jié)構(gòu)上是有兩個維度的,一個維度是同一樹枝上使用過,一個維度是同一樹層上使用過。
回看一下題目,元素在同一個組合內(nèi)是可以重復(fù)的,怎么重復(fù)都沒事,但兩個組合不能相同。所以我們要去重的是同一樹層上的“使用過”,同一樹枝上的都是一個組合里的元素,不用去重。
用示例中的[1, 2, 2] 來舉例,如圖所示:
注意去重需要先對集合排序
從圖中可以看出,同一樹層上重復(fù)取2 就要過濾掉,同一樹枝上就可以重復(fù)取2,因為同一樹枝上元素的集合才是唯一子集!
class Solution:def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:res = [] #存放符合條件結(jié)果的集合path = [] #用來存放符合條件結(jié)果def backtrack(nums,startIndex):res.append(path[:])for i in range(startIndex,len(nums)):if i > startIndex and nums[i] == nums[i - 1]: #我們要對同一樹層使用過的元素進(jìn)行跳過continuepath.append(nums[i])backtrack(nums,i+1) #遞歸path.pop() #回溯nums = sorted(nums) #去重需要排序backtrack(nums,0)return res
簡化的python寫法:
class Solution(object):def subsetsWithDup(self, nums):res = []nums.sort()self.dfs(nums, 0, res, [])return resdef dfs(self, nums, index, res, path):if path not in res: #判斷是否已經(jīng)存在res.append(path)for i in range(index, len(nums)):if i > index and nums[i] == nums[i - 1]:continueself.dfs(nums, i + 1, res, path + [nums[i]])
排列問題
46. 全排列
給定一個不含重復(fù)數(shù)字的數(shù)組 nums ,返回其 所有可能的全排列 。你可以 按任意順序 返回答案。
示例:
輸入:nums = [1,2,3]
輸出:[[1,2,3],[1,3,2],[2,1,3],[2,3,1],[3,1,2],[3,2,1]]
輸入:nums = [0,1]
輸出:[[0,1],[1,0]]
解題思路
總結(jié)搜索的方法:按順序枚舉每一位可能出現(xiàn)的情況,已經(jīng)選擇的數(shù)字在 當(dāng)前 要選擇的數(shù)字中不能出現(xiàn)。按照這種策略搜索就能夠做到 不重不漏。這樣的思路,可以用一個樹形結(jié)構(gòu)表示,用回溯的思想來進(jìn)行深度優(yōu)先搜索。
class Solution:def permute(self, nums) :res = []def backtrack(nums, path):# nums是未被選中的數(shù)的數(shù)組# tmp是已經(jīng)在path中if not nums:res.append(path)return for i in range(len(nums)):backtrack(nums[:i] + nums[i+1:], path + [nums[i]])backtrack(nums, [])return res
47. 全排列II
給定一個可包含重復(fù)數(shù)字的序列 nums ,按任意順序 返回所有不重復(fù)的全排列。
示例 1:
輸入:nums = [1,1,2]
輸出:
[[1,1,2], [1,2,1], [2,1,1]]
解題思路
和46.全排列不同的是,該題需要考慮如何去重,去重的概念在本篇的90.子集II已經(jīng)涉及到。
那么“使用過”在這個樹形結(jié)構(gòu)上是有兩個維度的,一個維度是同一樹枝上使用過,一個維度是同一樹層上使用過。
那么我們就要優(yōu)先考慮重復(fù),再思考 回溯的三要素。要強調(diào)的是去重一定要對元素經(jīng)行排序,這樣我們才方便通過相鄰的節(jié)點來判斷是否重復(fù)使用了。
那么這部分剪枝的條件即為:和前一個元素值相同(此處隱含這個元素的index>0),并且前一個元素還沒有被使用過
對于排列問題,樹層上去重和樹枝上去重,都是可以的,但是樹層上去重效率更高重。如下圖所示,圖片來源leetcode代碼隨想錄。
用輸入: [1,1,1] 來舉一個例子:
- 樹層上去重(used[i - 1] == false) 的樹形結(jié)構(gòu)如下:
- 樹枝上去重(used[i - 1] == true) 的樹型結(jié)構(gòu)如下:
為什么used[i - 1] == false,說明同一樹層nums[i - 1]使用過?
(i>0 && nums[i] == nums[i-1] && used[i-1] == false)
同一樹層我的理解是這樣,首先看第一個條件 i>0,這個條件的話說明此時已經(jīng)對第一個位置放的第一個數(shù)字的搜索已經(jīng)完畢,那么下一步要搜索能放在第一個位置的第二個數(shù)字,我們已經(jīng)知道不能放重復(fù)的數(shù)字,而因為數(shù)組已經(jīng)排序過,相同的數(shù)字used[i]都為0,因此通過nums[i] == nums[i-1]可以找到相同的數(shù)字。
最后這個條件 used[i-1] == false比較難理解,你可以理解為[1,1,1]中的第一個數(shù)字1已經(jīng)用過一次經(jīng)歷過used[i]的0->1->0的過程變化,所以此時i>0的是判斷條件通過進(jìn)到下一個條件判斷,最后到 used[i-1] == false
class Solution:def permuteUnique(self, nums: List[int]) -> List[List[int]]:# res用來存放結(jié)果 if not nums: return []nums = sorted(nums)#排序res = []used = [0] * len(nums) #是否用過,方便去重def backtracking(nums, used, path):# 終止條件if len(path) == len(nums):res.append(path)res.append(path.copy())returnfor i in range(len(nums)):if not used[i]:if i>0 and nums[i] == nums[i-1] and not used[i-1]:continueused[i] = 1# path.append(nums[i])# backtracking(nums, used, path)backtracking(nums, used, path + [nums[i]])# path.pop()used[i] = 0backtracking(nums,used,[])return res
總結(jié)
以上是生活随笔為你收集整理的【Leetcode】刷题之路4(python版)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【目标检测】yolo系列:从yolov1
- 下一篇: 栈和队列在python中的实现