問題描述:【Math】60. Permutation Sequence
解題思路:
這道題是一個從 1 到 n 的數(shù)組,共有 n! 個全排列序列,找到第 k 個全排列序列。
剛開始沒仔細(xì)讀題就之間 DFS 回溯,很快寫好但超時了哈哈哈。再讀一下題目,發(fā)現(xiàn)如果 k 很大,要一個個找過去,所以會很慢。
實(shí)際上,這是一道數(shù)學(xué)題(找規(guī)律)。以 n = 5 舉例,共有 5!= 120 個排列,我們發(fā)現(xiàn):當(dāng) k <= 24,排列全部以 1 開頭;24 < k <= 48,排列全部以 2 開頭,以此類推。因此,我們可以一位一位的構(gòu)造答案,根據(jù) k 值判斷其落在哪個區(qū)間,找到開頭數(shù)字加入結(jié)果;然后,從數(shù)組中刪除該開頭數(shù)字,并確定 k 值位于當(dāng)前區(qū)間的第幾個,更新 k 值;按照上述方法進(jìn)行操作,直到得到一個全排列,就是最后的答案。
以 n = 5,k = 31 為例(nums = [1,2,3,4,5]):
- k ∈ (4!, 2*4!],則確定 ans = "2",數(shù)組中刪除 2(nums=[1,3,4,5]),k 更新為 k = 31 - 4! = 7 (k 位于該區(qū)間的第 7 個);
- k ∈ (3!, 2*3!],則確定 ans = "23",數(shù)組中刪除 3(nums=[1,4,5]),k 更新為 k = 7 - 3! = 1 (k 位于該區(qū)間的第 1 個);
- k ∈ (0, 1*2!],則確定 ans = "231",數(shù)組中刪除 1(nums=[4,5]),k 更新為 k = 1 - 0 = 1 (k 位于該區(qū)間的第 1 個);
- k ∈ (0, 1*1!],則確定 ans = "2314",數(shù)組中刪除 4(nums=[4]),k 更新為 k = 1 - 0 = 1 (k 位于該區(qū)間的第 1 個);
- k ∈ (0, 1*0!],則確定 ans = "23145",數(shù)組中刪除 5(nums=[]),k 更新為 k = 1 - 0 = 1 (k 位于該區(qū)間的第 1 個);
- 得到一個全排列,最終結(jié)果 ans = "23145"
Python3 實(shí)現(xiàn):
class Solution:
def getPermutation(self, n: int, k: int) -> str:
ans = ""
nums = [(i + 1) for i in range(n)]
while len(ans) != n: # 迭代方法,逐個字符加入到結(jié)果中
nf = math.factorial(len(nums) - 1)
for i in range(len(nums)):
if k <= (i + 1) * nf: # 找到k位于哪個區(qū)間
ans += str(nums[i])
del nums[i]
k -= i * nf # k位于當(dāng)前區(qū)間的第幾個
break
return ans
問題描述:【DFS】79. Word Search
解題思路:
這道題是給一個 m*n 的字符矩陣 board 和一個單詞 word,判斷 word 是否存在字符矩陣中。
這道題很明顯用 DFS 回溯法去解決。做法如下:
- 在主函數(shù)中,遍歷字符矩陣的每個坐標(biāo) (i, j),如果發(fā)現(xiàn)
board[i][j] == word[0]
,則將 board[i][j] 位置先改為 ""(因?yàn)槊總€位置字符只能使用一次),然后進(jìn)入回溯函數(shù)search(i, j, path, wind)
(path 記錄單詞路徑,wind 為 word 索引)進(jìn)行深搜判斷。如果沒有在 search() 函數(shù)中找到,說明沒有以當(dāng)前字符 board[i][j] 開頭的單詞,要恢復(fù)原來該位置的字符。 - 在回溯函數(shù)中,對于每個字符的上下左右四個位置進(jìn)行深搜(要保證不越界),如果 board 的下一個位置的字符匹配 word 的下一個字符,則修改 board 中當(dāng)前字符為 "" 進(jìn)行遞歸調(diào)用。遞歸調(diào)用結(jié)束后,要先恢復(fù)原來該位置的字符,再去判斷返回值是 True 還是 False。如果找到(返回值為 True,則返回 True),否則繼續(xù)查找下一個位置。
Python3 實(shí)現(xiàn):
class Solution:
def exist(self, board: List[List[str]], word: str) -> bool:
def search(i, j, path, wind): # (i,j)是匹配word第一個字符的坐標(biāo),path記錄結(jié)果,wind為word索引
isfind = False
if path == word: # 找到了該單詞
return True
for p in pos:
if 0 <= i+p[0] < m and 0 <= j+p[1] < n and board[i+p[0]][j+p[1]] == word[wind]:
tmp = board[i+p[0]][j+p[1]]
board[i+p[0]][j+p[1]] = ""
isfind = search(i+p[0], j+p[1], path+word[wind], wind+1)
board[i+p[0]][j+p[1]] = tmp # 先恢復(fù)原來的字符
if isfind == True: # 再判斷返回結(jié)果,如果是True直接返回;如果是False繼續(xù)查找
return True
return False
if len(word) == 0 or len(board) == 0:
return False
pos = [[-1,0], [1,0], [0,-1], [0,1]] # 上下左右四個位置
m = len(board)
n = len(board[0])
for i in range(m):
for j in range(n):
if board[i][j] == word[0]:
tmp = board[i][j]
board[i][j] = ""
if search(i, j, word[0], 1):
return True
board[i][j] = tmp # 如果沒有找到,恢復(fù)原來的字符
return False
問題描述:【DFS】93. Restore IP Addresses
解題思路:
這道題是給一個數(shù)字字符串,返回所有可能的 IP 地址組合。
這道題和下面的 Leetcode 131 以及 Leetcode 842 做法是類似的,也是使用回溯法 DFS 對字符串前綴進(jìn)行劃分。注意該深搜函數(shù) search(s, path)
(s 為后半部分字符串,path 為劃分的 IP 子段)的幾個出口:
- 如果 len(path) > 4,不符合 IP 地址,提前終止,返回;
- 如果 s 為空串,但是 len(path) < 3,說明劃分結(jié)束,但是不符合 IP 地址,也返回;
- 如果 s 為空串,且 len(path) == 4,說明找到一組解,就加入到結(jié)果列表 ans 中。
在 for 循環(huán)中,還要注意去除前導(dǎo) 0 以及字符串前綴數(shù)字 > 255 的情況,才能進(jìn)行遞歸調(diào)用深搜。
Python3 實(shí)現(xiàn):
class Solution:
def restoreIpAddresses(self, s: str) -> List[str]:
def search(s, path):
if len(path) > 4: # 不滿足題意
return
if not s and len(path) < 4: # 不滿足題意
return
if not s and len(path) == 4: # 找到一組解
ans.append('.'.join(path))
return
for i in range(1, len(s) + 1):
pre = s[:i] # 劃分前綴
if (len(pre) > 1 and pre[0] == '0') or int(pre) > 255: # 不滿足題意
continue
search(s[i:], path + [pre])
ans = []
search(s, [])
return ans
問題描述:【Brute Force、DFS】131. Palindrome Partitioning
解題思路:
這道題是給一個字符串,將字符串分割成一些子串,使得所有子串都是回文串,求所有劃分的方案數(shù)。
一個子串是否是回文串可以使用 s == s[::-1]
來判斷。
方法1(Brute Force):
首先想到一種暴力解法,就是對于字符串的每個字符 s[i],依次將 s[i] 加入到回文串前綴列表中每個回文串前綴的后面,然后再判斷 s[i] 的加入能否形成新的回文串前綴。如果可以,拓展回文串前綴列表。最后,遍歷完所有字符后,列表中存儲的就是最終結(jié)果。
以 s = "abba" 舉例:
- s[0] = a,a 本身是回文串,加入到結(jié)果列表 ans = [[a]];
- s[1] = b,b 加入回文串前綴的后面,得到 ans = [[a,b]];
- s[2] = b,b 先加入回文串前綴的后面,得到 ans = [[a,b,b]];然后發(fā)現(xiàn),b 的加入可以形成新的回文串 "bb"(從最后一個 b 開始往前形成子串 bb),因此拓展結(jié)果列表得到 ans = [[a,b,b], [a,bb]];
- s[3] = a,a 先加入回文串前綴的后面,得到 ans = [[a,b,b,a], [a,bb,a]];然后發(fā)現(xiàn),a 的加入可以形成新的回文串 "abba"(注意到 ans 中兩個都可以構(gòu)成 "abba",所以還要判斷新加入的回文串是否之前已經(jīng)加入過),因此拓展結(jié)果列表得到 ans = [[a,b,b,a], [a,bb,a], [abba]]。
這種做法時間復(fù)雜度為 O(n^4),空間復(fù)雜度為 O(n^2),能夠 AC。
Python3 實(shí)現(xiàn):
class Solution:
def partition(self, s: str) -> List[List[str]]:
ans = [[]]
for i in range(len(s)):
for a in ans: # 將當(dāng)前字符加入到每個字符串前綴列表的后面
a.append(s[i])
for a in ans:
N = len(a)
st = a[-1]
for j in range(N-2, -1, -1):
st = a[j] + st # 從最后一個字符開始構(gòu)造新的子串
if st == st[::-1] and a[:j] + [st] not in ans: # 判斷新的子串是否是回文串,且構(gòu)成的回文串前綴是否之前出現(xiàn)過
ans.append(a[:j] + [st]) # 將新的回文串前綴加入到結(jié)果列表中
return ans
方法2(DFS):
其實(shí)這道題一看是求所有結(jié)果,很明顯用 DFS 回溯法求解,但是剛開始沒有思路,找不到如果求解的方法,最后看了別人的做法才明白。
使用回溯法的解題思路是對于字符串 s 的前綴進(jìn)行劃分,然后判斷前綴是否是回文子串。如果是,形成臨時結(jié)果,將 s 的后半部分和臨時結(jié)果傳入到下一層(深搜);如果不是,那就繼續(xù)劃分下一個前綴。最后,傳入的 s 會變成空串,這時形成的結(jié)果必定是回文串的一個劃分,加入到結(jié)果列表中即可。
以 s = "aab" 舉例,用 path 記錄一種劃分結(jié)果:
- 1、劃分 s = "aab" 前綴 a,a 是回文串,將其加入 path = [a],并且同 s = "ab" 傳入到下一層;
- 2、劃分 s = "ab" 前綴 a,a 是回文串,將其加入 path = [a,a],并且同 s = "b" 傳入到下一層;
- 3、劃分 s = "b" 前綴 b,b 是回文串,將其加入 path = [a,a,b],并且同 s = "" 傳入到下一層;
- 4、因?yàn)?s = "",說明劃分完畢,將結(jié)果 path = [a,a,b] 加入到 ans 中,直接返回到步驟 3(沒有前綴,繼續(xù)返回)、步驟 2;
- 5、在步驟 2 中,劃分 s = "ab" 前綴 ab,ab 不是回文串,繼續(xù)劃分下一個前綴;沒有前綴,返回步驟 1;
- 6、在步驟 1 中,劃分 s = "aab" 前綴 aa,aa 是回文串,將其加入 path = [aa],并且同 s = "b" 傳入到下一層;
- 7、劃分 s = "b" 前綴 b,b 是回文串,將其加入 path = [aa,b],并且同 s = "" 傳入到下一層;
- 8、因?yàn)?s = "",說明劃分完畢,將結(jié)果 path = [aa,b] 加入到 ans 中,直接返回到步驟 7(沒有前綴,繼續(xù)返回)、步驟 6;
- 9、在步驟 6 中,劃分 s = "aab" 前綴 aab,aab 不是回文串,繼續(xù)劃分下一個前綴;沒有前綴,結(jié)束。
- 10、最終得到結(jié)果 ans = [[a,a,b], [aa,b]]。
優(yōu)化:可以通過用區(qū)間 DP 來計算任意 s[i:j] 之間是否是回文串 【區(qū)間DP、雙指針】647. Palindromic Substrings,并保存結(jié)果;然后再執(zhí)行DFS,如果發(fā)現(xiàn)某條子串不是回文,就可以直接退出,從而減少計算量。
Python3 實(shí)現(xiàn):
class Solution:
def partition(self, s: str) -> List[List[str]]:
def search(s, path):
if not s: # s 變?yōu)榭沾瑢⑿纬傻囊环N結(jié)果path加入到ans中
ans.append(path[:])
for i in range(1, len(s) + 1):
pre = s[:i] # 對于s的每一個前綴
if pre == pre[::-1]: # 如果前綴是回文串,則深搜
search(s[i:], path + [pre]) # 將后半部分和臨時結(jié)果傳入到下一層
ans = []
search(s, [])
return ans
問題描述:【DFS】842. Split Array into Fibonacci Sequence
解題思路:
這道題是給一個數(shù)字字符串 S,將其劃分成斐波那契序列。
很容易想到用 DFS 回溯法去找斐波那契序列的一種劃分。類似于上面的 Leetcode 131,對于數(shù)字字符串 S 的前綴進(jìn)行劃分,如果 S 的前綴合法(沒有前導(dǎo) 0 并且數(shù)字沒有越界),就將 S 的后半部分和臨時結(jié)果 path 傳入,進(jìn)行遞歸調(diào)用。
遞歸出口:
- 如果臨時結(jié)果 path 長度大于等于 3,但是不滿足 f[i-2] + f[i-1] = f[i],返回 False;
- 如果臨時結(jié)果 path 長度大于等于 3,且 S 為空串,說明劃分完畢,保存結(jié)果
ans.extend(path)
,返回 True。
第一次提交時,WA 了,報錯如下:
檢查了一下發(fā)現(xiàn)沒什么問題啊?然后重新讀題,很敏感的發(fā)現(xiàn)斐波那契序列數(shù)值要求為 <= 2 ** 31 - 1(2147483647),然后發(fā)現(xiàn)報錯結(jié)果的最后一項(xiàng) 11336528511 > 2 ** 31 - 1,因此在代碼中加入了數(shù)值范圍的判斷,就 AC 了。
Python3 實(shí)現(xiàn):
class Solution:
def splitIntoFibonacci(self, S: str) -> List[int]:
def search(S, path):
if len(path) >= 3 and path[-3] + path[-2] != path[-1]: # 不滿足斐波那契條件
return False
if not S and len(path) >= 3: # S為空串,說明可以劃分
ans.extend(path) # 找到一種劃分,保存結(jié)果到ans中,返回
return True
for i in range(1, len(S) + 1):
pre = S[:i] # 劃分前綴
if (len(pre) > 1 and pre[0] == '0') or (int(pre) > 2 ** 31 - 1): # 不滿足題意
continue
if search(S[i:], path + [int(pre)]): # 將S后半部分和臨時結(jié)果傳入進(jìn)行遞歸調(diào)用
return True
return False # S本身長度小于3
ans = []
search(S, [])
return ans