1. 程式人生 > 其它 >回溯演算法解題框架

回溯演算法解題框架

這兩天在刷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])

總結

方法二功底很深厚,學習學習