回溯演算法解題框架
這兩天在刷Leetcode N皇后和單詞搜尋時,探究回溯演算法的解題思路,結合資料整理框架模版,便於總結和參考。解決回溯問題,就是解決決策樹問題,當前的決策對後面的選擇至關重要,話說條條大路通羅馬,每一次決策過程就是遍歷一條可走得通的路,但是演算法是有條件的,往往只有一條几條路走得通。
在決策過程中過程中,要考慮三個問題:
- 路徑選擇: 已經做出的選擇
- 選擇列表: 所有可供的選擇
- 結束條件: 也就是決策樹底層,無法再做出其他選擇
程式碼模版則是:
result = [] def backtrack(路徑, 選擇列表): if 滿足條件: result.add(路徑) return for 選擇 in 選擇列表: 做選擇 backtrack(路徑, 選擇列表) 撤銷選擇
暫時不理解沒關係,我也是在做題時把模版往題上套才慢慢明白。
LeetCode 79 單詞搜尋
題目描述:
給定一個m x n 二維字元網格board 和一個字串單詞word 。如果word 存在於網格中,返回 true ;否則,返回 false 。
單詞必須按照字母順序,通過相鄰的單元格內的字母構成,其中“相鄰”單元格是那些水平相鄰或垂直相鄰的單元格。同一個單元格內的字母不允許被重複使用。
示例 1:
輸入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
輸出:true
示例 2:
輸入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE"
輸出:true
示例 3:
輸入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCB"
輸出:false
分析
本題從網格中相鄰的字串中找出word,題目要求同一個單元格的字母不允許重複,如上圖示例1,找到第一個’C' 時,即可以繼續向右邊尋找,結果是‘E',不對,又可以返回向下尋找’C‘,對了,繼續在第二個’C'的相鄰單元格中尋找,就是上述模版:
做選擇-〉判斷是否滿足要求-〉撤銷選擇的過程
程式碼模版可以大致為:
result = []
def backtrack(word, board):
# 滿足 return False
if board[i][j] != word[k]:
return 失敗
if 找到 word:
return 成功
# 網格中遍歷
for 選擇 in 選擇列表:
做選擇
backtrack(路徑, 選擇列表)
撤銷選擇
程式碼解析
class Solution:
def exist(self, board: List[List[str]], word: str) -> bool:
self.board = board
self.word = word
if self.board == None:
return False
self.h = len(self.board)
self.w = len(self.board[0])
self.help_list = [[0] * len(self.board[0]) for _ in range(len(self.board))]
for i in range(len(board)):
for j in range(len(board[0])):
if self.backtrack(i, j, 0):
return True
print(self.help_list)
return False
def backtrack(self, i, j, k):
if self.board[i][j] != self.word[k]:
return False
self.help_list[i][j] = 1
if k == len(self.word) - 1:
return True
# 方法一: 先設定方向,利用陣列選擇方向
# direction = [(0,1), (0,-1), (-1,0), (1,0)]
# for di, dj in direction:
# newdi, newdj = i + di, j + dj
# if 0 <= newdi < len(self.board) and 0 <= newdj < len(self.board[0]):
# if self.help_list[newdi][newdj] == 0:
# if self.backtrack(newdi, newdj, k + 1):
# result = True
# break
# 方法二: 計算方向
# 回溯法原理,只要有其中一個結果符合就進行下一步操作, 不符合這試試相鄰的, 結果之間不互斥,
# 但是所有結果都不符合則撤銷該選擇,返回False
if 0 <= j+1 < self.w and self.help_list[i][j+1] == 0:
if self.backtrack(i, j+1, k+1):
return True
if self.w > j-1 >= 0 and self.help_list[i][j-1] == 0:
if self.backtrack(i, j-1, k+1):
return True
if self.h > i-1 >= 0 and self.help_list[i-1][j] == 0:
if self.backtrack(i-1, j, k+1):
return True
if 0 <= i+1 < self.h and self.help_list[i+1][j] == 0:
if self.backtrack(i+1, j, k+1):
return True
self.help_list[i][j] = 0
return False
總結
上述程式碼中方法一程式碼更簡潔,方向上利用陣列複製,避免了方法二的程式碼冗餘。
- 在遇到類似
方向
相關的題是用陣列改變方向靈活性強。 - 回溯問題每一個子決策之間不互斥,只要有一個走通就可以返回True
- 決策不成功則
還原現場
LeetCode 51 N皇后
題目描述:
n皇后問題 研究的是如何將 n個皇后放置在 n×n 的棋盤上,並且使皇后彼此之間不能相互攻擊。
給你一個整數 n ,返回所有不同的n皇后問題 的解決方案。
每一種解法包含一個不同的n 皇后問題 的棋子放置方案,該方案中 'Q' 和 '.' 分別代表了皇后和空位。
示例 1:
輸入:n = 4
輸出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解釋:如上圖所示,4 皇后問題存在兩個不同的解法。
示例 2:
輸入:n = 1
輸出:[["Q"]]
分析
皇后彼此不能相互攻擊,也就是說:任何兩個皇后都不能處於同一條橫行、縱行或斜線上。假定斜線分別為撇和捺,位於撇線
上的座標相加等於一個固定值,位於捺線
上的座標相減等於一個固定值。如下圖所示,該皇后的輻射範圍為:
下一個皇后在放置前,先判斷是否在其他皇后攻擊範圍之內。
路徑:board 中⼩於當前行row的那些⾏都已經成功放置了皇后
選擇列表:當前行row的所有列都是放置皇后的選擇
結束條件:row 超過 board 的最後⼀⾏
程式碼解析
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
if n < 1: return []
self.n = n
# 方法一:
# self.result = []
# self.col = set()
# self.pei = set()
# self.na = set()
# self.dfs1(n, 0, [])
# return self._generate_result(n)
# 方法二:
self.result = []
self._dfs2([], [], [])
return [["." * i + "Q" + "." * (n - 1 - i) for i in col] for col in self.result]
# 方法一:
def dfs1(self, n, row, current_col):
if row >= n:
self.result.append(current_col)
return
for col in range(n):
# 尋找合適位置
if col in self.col or col + row in self.pei or row - col in self.na:
continue
# 加入條件
self.col.add(col)
self.pei.add(row + col)
self.na.add(row - col)
# 尋找下一行合適的位置,current_col 存放每一次合適的列元素索引
self.dfs(n, row + 1, current_col + [col])
# 如果下一層執行到col = n 則返回到此步,沒有合適位置,執行後面一列
self.col.remove(col)
self.pei.remove(row + col)
self.na.remove(row - col)
def _generate_result(self, n):
b = []
for res in self.result:
for i in res:
b.append("." * i + "Q" + "." * (n - 1 - i))
return [b[i: i + n] for i in range(0, len(b), n)]
# 方法二: xy_diff=pei, xy_sum = na
def _dfs2(self, queue, xy_diff, xy_sum):
# 行,每一次從這裡更新,如果上一個操作滿足, 則行 + 1
p = len(queue)
if p == self.n:
self.result.append(queue)
# 列, queue
for q in range(self.n):
# queue 存放q(列)的值
# 此時p = len(queue) 已經到下一行,從下一行的第一列開始計算 p-q 和 p+q.
if q not in queue and p - q not in xy_diff and p + q not in xy_sum:
self._dfs2(queue + [q], xy_diff + [p - q], xy_sum + [p + q])
總結
方法二功底很深厚,學習學習