回溯使用的場景:
回溯法非常適合由多個步驟組成的問題,並且每個步驟都有多個選項,當我們在某一步選擇了其中一個選項時,就進入下一步,然後又面臨新的選項。我們就這樣重複選擇着,直至到達最後的狀態。
一般畫樹狀圖表示
做回溯的題步驟:【樹的深度遍歷過程】
- 畫圖,觀察元素是否有重複,如有重複則需要剪枝,思考如何剪枝
- 回溯三要素:路徑、選擇列表、結束條件
- 按照此代碼模板,寫出代碼。
算法框架
廢話不多說,直接上回溯算法框架。解決一個回溯問題,實際上就是一個決策樹的遍歷過程。你只需要思考 3 個問題:
1、路徑:也就是已經做出的選擇。
2、選擇列表:也就是你當前可以做的選擇。
3、結束條件:也就是到達決策樹底層,無法再做選擇的條件。
代碼方面,回溯算法的框架:
result = []
def backtrack(路徑, 選擇列表):
if 滿足結束條件:
result.add(路徑)
return
for 選擇 in 選擇列表:
做選擇
backtrack(路徑, 選擇列表)
撤銷選擇
其核心就是 for 循環裏面的遞歸,在遞歸調用之前「做選擇」,在遞歸調用之後「撤銷選擇」,特別簡單。
剪枝方法:
- 全排列裏面的剪枝,只需要考慮後面的元素和第一個元素相同的情況,則continue。但是要注意是要保證同層不同和上下層相同(i > start),詳見leetcode40.總結
- 對於全排列題不適合用mark,mark用在矩陣中元素只訪問一次的情況下如訪問矩陣裏的座標或者字母題。
注意事項
1.如果是選擇列表循環內需要判斷,用continue(表示這步不行還有機會進行下一步),如果是在選擇列表循環外判斷則用return中斷
2.首先對當前路徑進行判斷,再進入下一個選擇列表中。這樣可以保證所有的狀態都得到判斷。
(劍指offer12,13有感而發)
做題順序
Leetcode:46,47,39,40,78,90,22
劍指offer:12,13
【leetcode46】全排列
#我一直和組合問題看成是一類問題,動作選擇弄成start,但是這題,動作選擇根本不適合start,因爲它還會選取到start之前的動作,因此,這道題應該把動作改成可選擇數字的列表。
#這一題沒有重複的情況,不需要剪枝。
class Solution:
def permute(self, nums: List[int]) -> List[List[int]]:
#如果有[1, 2, 3],第一次選擇1,之後選擇2,3,那麼這種可以用start
#但是對於這道題,第一次選擇2,第二次可以選擇1,3,這種就沒必要用start
result = []
size = len(nums)
if size == 0:#終止條件
return result
def backtrack(path, nums):
'''
:param path: 列表
:param nums: 動作選擇列表
結束條件:len(path)==size
'''
if len(path) == size:
result.append(copy.deepcopy(path))
return
for i in range(len(nums)):#一層有多少個可能性就有多少個循環
#path.append(nums[i])
backtrack(path + [nums[i]], nums[:i]+nums[i+1:])
#path.pop()#回溯
backtrack([], nums)
return result
【leetcode46】全排列2
#回溯+剪枝法
class Solution:
def permuteUnique(self, nums: List[int]) -> List[List[int]]:
result = []
size = len(nums)
if size == 0:
return result
nums.sort() #提前排序
def backtrack(path, nums):
if len(path) == size:
result.append(copy.deepcopy(path)) #注意這必須是深複製,防止遞歸過程中對它影響
return
for i in range(len(nums)):#每一層有幾種情況循環幾次
if i > 0 and nums[i] == nums[i-1]: #i>0保留第一個,使得重複的數字運行一次【剪枝+邊界條件】
continue
#path.append(nums[i])
backtrack(path + [nums[i]], nums[:i]+nums[i+1:])
#path.pop()#回溯
backtrack([], nums)
return result
#和上一題相比,這題有重複的,因此需要剪枝,並且將序列排序。
【leetcode39】組合
class Solution:
def combinationSum(self, candidates: List[int], target: int) -> List[List[int]]:
result = []
size = len(candidates)
if size == 0:
return result
def backtrack(path, start, residue):
if residue == 0:
result.append(copy.deepcopy(path))
return
if residue < 0:
return
for c in range(start, size): #例如一共有[2, 3, 6, 7]第一次選擇3,第二次只能從3開始選,就不能倒回去選擇2了。
#residue -= candidates[i] 這樣是不對的,不能改變residue的值
#path.append(candidates[c])
# print(path)
backtrack(path + [candidates[c]], c, residue - candidates[c]) ##注意residue-candidate[i]要寫在遞歸函數裏面
#path.pop()
backtrack([], 0, target)
return result
【leetcode40】組合2
這道題在第二次做的時候仍然出了問題。問題在於剪枝的條件
if i > start and candidates[i-1] == candidates[i]:
continue
和中斷的條件 if s < 0 or start >= size:
參考這位大佬的,寫的非常好https://leetcode-cn.com/problems/combination-sum-ii/solution/xiang-xi-jiang-jie-ru-he-bi-mian-zhong-fu-by-allen/
與上一道題回朔完全相同,差的只是一個如何避免重複的問題
這個避免重複當思想是在是太重要了。
這個方法最重要的作用是,可以讓同一層級,不出現相同的元素。但是卻允許了不同層級之間的重複
1
/ \
2 2 這種情況不會發生
/ \
5 5
例2
1
/
2 這種情況確是允許的
/
2
爲何會有這種神奇的效果呢?
首先 cur-1 == cur 是用於判定當前元素是否和之前元素相同的語句。這個語句就能砍掉例1。可是問題來了,如果把所有當前與之前一個元素相同的都砍掉,那麼例二的情況也會消失。 因爲當第二個2出現的時候,他就和前一個2相同了。
那麼如何保留例2呢?
那麼就用cur > begin 來避免這種情況,你發現例1中的兩個2是處在同一個層級上的,
例2的兩個2是處在不同層級上的。在一個for循環中,所有被遍歷到的數都是屬於一個層級的。我們要讓一個層級中,必須出現且只出現一個2,那麼就放過第一個出現重複的2,但不放過後面出現的2。第一個出現的2的特點就是 cur == begin. 第二個出現的2 特點是cur > begin.
class Solution:
def combinationSum2(self, candidates: List[int], target: int) -> List[List[int]]:
result = []
size = len(candidates)
if size == 0:
return result
candidates.sort()
def backtrack(path, start, residue):
if residue == 0:
result.append(copy.deepcopy(path))
return
if residue < 0 or start >= size:#記得要把所有的中斷找全
return
for c in range(start, size):
#如果c必須要先大於start才能保證c-1這個下界。
if c > start and candidates[c-1] == candidates[c]:#for循環時同一層的,不讓同一層的重複,但不同層可以重複。詳解見https://leetcode-cn.com/problems/combination-sum-ii/solution/xiang-xi-jiang-jie-ru-he-bi-mian-zhong-fu-by-allen/
continue
#path.append(candidates[c])
#當有i+1要小心了,start可能會出界,所以加上這個中斷條件
backtrack(path + [candidates[c]], c+1, residue - candidates[c])
#path.pop()
backtrack([], 0, target)
return result
【leetcode78】子集
思路:畫出圖以後發現,所有的節點都符合要求。因此每一個葉子都加入result
class Solution:
def subsets(self, nums: List[int]) -> List[List[int]]:
result = []
size = len(nums)
if size == 0:
return result
def backtrack(path, start):
result.append(path)
if start == size:
return
for i in range(start, size):
backtrack(path+[nums[i]], i+1)
backtrack([], 0)
return result
【leetcode90】子集2
又遇到了剪枝的問題。畫圖發現,1,2,2的時候兩個2保留第一個2就行了。
class Solution:
def subsetsWithDup(self, nums: List[int]) -> List[List[int]]:
result = []
size = len(nums)
if size == 0:
result.append([])
return
nums.sort()
def backtrack(path, start):
result.append(path) #每一片葉子都是我要的答案
if start == size:
return
for i in range(start, size):
if i > start and nums[i-1] == nums[i]: #保證同層不重複,異層可以相同
continue
backtrack(path + [nums[i]], i + 1)
backtrack([], 0)
return result
[leetcode79]單詞搜索—— [劍指offer]12題
backtrack有返回值的好像就這一道,我覺得還是有難度的哈哈哈哈哈哈。
mark用來標記矩陣元素, 0表示沒有訪問,1表示訪問過了。
思路:
- 一種是backtrack(i, j, word)對當前i,j進行判斷,如果可以往下進行,則下一步,如果不可以,退一步(用mark來表示)
- 一種是對i,j的下一步cur_i, cur_j進行判斷,如果可以往下進行,則下一步,如果不可以,退一步(用mark來表示)
注意:
- 在backtrack裏面,在循環選擇列表裏只能用continue在循環列表外只能用return。
- 對於此題來說,找到一個word,摒棄一個,要注意遞歸的返回值的正確應用。
思路1
class Solution(object):
def exist(self, board, word):
row = len(board)
col = len(board[0])
if row == 0:
return False
mark = [[0 for _ in range(col)] for _ in range(row)]
def backtrack(i, j, mark, word):
if len(word) == 0:
return True
if not (i >= 0 and j >= 0 and i < row and j < col):
return False
if mark[i][j] == 1:
return False
if board[i][j] == word[0]:
mark[i][j] = 1
#函數有返回值要先賦值。
fanhuiz = backtrack(i+1, j, mark, word[1:]) or backtrack(i-1, j, mark, word[1:]) or backtrack(i, j-1, mark, word[1:]) or backtrack(i, j+1, mark, word[1:])
#這個不能直接if not (backtrack(i+1, j, mark, word[1:]) or backtrack(i-1, j, mark, word[1:]) or backtrack(i, j-1, mark, word[1:]) or backtrack(i, j+1, mark, word[1:]))
if not fanhuiz:
mark[i][j] = 0 #回溯,因爲這個點沒走下去,因此這個點相當於沒走
return False
else:
return True
return False
for i in range(row):
for j in range(col):
if backtrack(i, j, mark, word):
return True
return False
思路2:
class Solution(object):
def exist(self, board, word):
directs = [(0, 1), (0, -1), (1, 0), (-1, 0)]
row = len(board)
col = len(board[0])
if row == 0:
return False
def backtrack(i, j, word, mark):
#中斷條件
if len(word) == 0:
return True
for direct in directs: #一層所有的選擇(上下左右)
cur_i = i + direct[0]
cur_j = j + direct[1]
if cur_i >= 0 and cur_i < row and cur_j >= 0 and cur_j < col and board[cur_i][cur_j] == word[0]:#這句是爲了看此時定位的字母是不是word的首字母
if mark[cur_i][cur_j] == 1:
continue
#回溯方法要記住
# 將該元素標記爲已使用
mark[cur_i][cur_j] = 1
if backtrack(cur_i, cur_j, word[1:], mark) is True:
return True
else:
mark[cur_i][cur_j] = 0
return False
mark = [[0 for _ in range(col)] for _ in range(row)]
for i in range(row):
for j in range(col):
if board[i][j] == word[0]:
#回溯方法要記住
mark[i][j] = 1
if backtrack(i, j, word[1:], mark) == True:#如果一條路走不通就是False
return True
else:
mark[i][j] = 0
return False
[劍指offer]13題 機器人的運動範圍
思路1:backtrack無返回值就不用處理了。
class Solution:
def movingCount(self, threshold, rows, cols):
self.count = 0
if rows == 0:
return self.count
mark = [[0 for _ in range(cols)] for _ in range(rows)]
def backtrack(i, j, k):
if i < 0 or i >= rows or j < 0 or j >= cols:
return
if mark[i][j] == 1:
return
tmpi = list(map(int, list(str(i))))
tmpj = list(map(int, list(str(j))))
if sum(tmpi) + sum(tmpj) > k:
return
mark[i][j] = 1
self.count += 1
backtrack(i-1, j, k)
backtrack(i+1, j, k)
backtrack(i, j-1, k)
backtrack(i, j+1, k)
backtrack(0, 0, threshold)
return self.count
思路2 :
一定要注意對於初始ij的處理。
class Solution:
def movingCount(self, threshold, rows, cols):
directs = [(0, 1), (0, -1), (1, 0), (-1, 0)]
self.count = 0
if rows == 0:
return self.count
mark = [[0 for _ in range(cols)] for _ in range(rows)]
mark[0][0] = 1
def backtrack(i, j, k):
for direct in directs:
cur_i = i + direct[0]
cur_j = j + direct[1]
if not (cur_i >= 0 and cur_i < rows and cur_j >= 0 and cur_j < cols):
continue
i_list = list(map(int, list(str(cur_i))))
j_list = list(map(int, list(str(cur_j))))
if sum(i_list) + sum(j_list) > k:
continue
if mark[cur_i][cur_j] == 1:
continue
self.count += 1
mark[cur_i][cur_j] = 1
backtrack(cur_i, cur_j, k)
if threshold > 0:
self.count = 1
backtrack(0, 0, threshold)
return self.count
總結:還有一些小細節,記錄一下遇到的坑。1、count= 0必須寫成self.count=0,而矩陣list不用。2、如果有bactrack有返回值,在遞歸判斷時應用3、map的對象必須是正整數,不能是負數,因此要先判斷i,j是否出界再進得到i_list, j_list。
[leetcode22] 括號的生成
思路:畫圖,想清楚路徑和中斷條件。
class Solution:
def generateParenthesis(self, n: int):
result = []
if n == 0:
return result
def helper(left, right, path):
if right > left:
return
if left == n and right == n:
result.append(path[:])
return
if left < n:
helper(left+1, right, path+'(')
if right < n:
helper(left, right+1, path+')')
helper(0, 0, '')
return result