1. 程式人生 > 實用技巧 >leetcode 51/52N皇后問題(dfs/回溯)

leetcode 51/52N皇后問題(dfs/回溯)

  • 題目描述
n皇后問題研究的是如何將 n個皇后放置在 n×n 的棋盤上,並且使皇后彼此之間不能相互攻擊。


上圖為 8 皇后問題的一種解法。

給定一個整數 n,返回所有不同的n皇后問題的解決方案。

每一種解法包含一個明確的n 皇后問題的棋子放置方案,該方案中 'Q''.' 分別代表了皇后和空位。

示例:

輸入:4
輸出:[
[".Q..", // 解法 1
"...Q",
"Q...",
"..Q."],

["..Q.", // 解法 2
"Q...",
"...Q",
".Q.."]
]
解釋: 4 皇后問題存在兩個不同的解法。

  • 解法一:套回溯法模板

解決一個回溯問題,實際上就是一個決策樹的遍歷過程

參考回溯法模板:

1、路徑:也就是已經做出的選擇。

2、選擇列表:也就是你當前可以做的選擇。

3、結束條件:也就是到達決策樹底層,無法再做選擇的條件。

虛擬碼回溯的框架:

result = []
def backtrack(路徑, 選擇列表):
    if 滿足結束條件:
        result.add(路徑)
        return

    for 選擇 in 選擇列表:
        做選擇
        backtrack(路徑, 選擇列表)
        撤銷選擇

皇后可以攻擊同一行、同一列、左上左下右上右下四個方向的任意單位,那麼N皇后問題則可以看成,決策樹的每一層表示棋盤上的每一行;每個節點可以做出的選擇是,在該行的任意一列放置一個皇后。

那麼直接套用框架的話,就是這樣:

vector<vector<string>> res;

/* 輸入棋盤邊長 n,返回所有合法的放置 */
vector<vector<string>> solveNQueens(int n) {
    // '.' 表示空,'Q' 表示皇后,初始化空棋盤。
    vector<string> board(n, string(n, '.'));
    backtrack(board, 0);
    return res;
}

// 路徑:board 中小於 row 的那些行都已經成功放置了皇后
// 選擇列表:第 row 行的所有列都是放置皇后的選擇
// 結束條件:row 超過 board 的最後一行 void backtrack(vector<string>& board, int row) { // 觸發結束條件 if (row == board.size()) { res.push_back(board); return; } int n = board[row].size(); for (int col = 0; col < n; col++) { // 排除不合法選擇 if (!isValid(board, row, col)) continue; // 做選擇 board[row][col] = 'Q'; // 進入下一行決策 backtrack(board, row + 1); // 撤銷選擇 board[row][col] = '.'; } }

其中對於皇后同一列,對角線,斜對角線的判斷可以用isVlalid函式表示:

  • 檢查列是否有皇后衝突(很好判斷,直接判斷某列是否等於Q)
  • 檢查右上方是否有皇后衝突(則判斷斜對角線是否有皇后,那麼斜對角線怎麼表示呢,則行索引從row-1遞減,列索引從col+1遞增)
  • 檢查左上方是否有皇后衝突(則判斷對角線是否有皇后,與上同)
/* 是否可以在 board[row][col] 放置皇后? */
bool isValid(vector<string>& board, int row, int col) {
    int n = board.size();
    // 檢查列是否有皇后互相沖突
    for (int i = 0; i < n; i++) {
        if (board[i][col] == 'Q')
            return false;
    }
    // 檢查右上方是否有皇后互相沖突
    for (int i = row - 1, j = col + 1; 
            i >= 0 && j < n; i--, j++) {
        if (board[i][j] == 'Q')
            return false;
    }
    // 檢查左上方是否有皇后互相沖突
    for (int i = row - 1, j = col - 1;
            i >= 0 && j >= 0; i--, j--) {
        if (board[i][j] == 'Q')
            return false;
    }
    return true;
}

那麼這兩段程式碼,用Python表示就是這樣的(這裡需要注意下Python的深拷貝和淺拷貝問題,回溯會修改board的值,需要用深拷貝之前的值):

import copy
class Solution:
    def solveNQueens(self, n: int) -> List[List[str]]:
        self.N = n
        self.res = []
        # board = ['.' for i in range(n)]
        board = [['.' for i in range(n)] for j in range(n)]
        self.backtrack(board, 0, self.res)
        return self.res

    def backtrack(self, board, row, res):
        if row == len(board):
            s = []
            for item in board:
                st = ''
                for it in item:
                    st += it
                s.append(st)
            res.append(copy.deepcopy(s))
            return

        n = len(board[row])
        for i in range(n):
            if not self.isValid(board, row, i):
                continue
            board[row][i] = 'Q'
            self.backtrack(board, row + 1, res)
            board[row][i] = '.'

    def isValid(self, board, row, col):
        num = len(board)
        #同一列是否衝突
        for i in range(num):
            if board[i][col] =='Q':
                return False
        i, j = row - 1, col + 1
        #右上方是否衝突
        while i >= 0 and j < num:
            if board[i][j] == 'Q':
                return False
            i -= 1
            j += 1
        #左上方是否衝突
        i, j = row - 1, col - 1
        while i >= 0 and j >= 0:
            if board[i][j] == 'Q':
                return False
            i -= 1
            j -= 1
        return True

但是這樣時間複雜度太高了:

執行用時:108 ms 記憶體消耗:13.9 MB 所以我們研究下解法二,直接用dfs,判斷皇后是否衝突的時候直接用一維陣列判斷可好?
  • 解法二:dfs

其實dfs的遞迴條件,判斷條件都可以想得到,而不好理解的地方是哪裡呢?就是下面這三個儲存N皇后行,對角線,斜對角線是否衝突的陣列

        col = [False] * n
        dg = [False] * 2 * n
        xdg = [False] * 2 * n

那麼,這裡為什麼要這麼設定需要結合官方題解去理解。對於N皇后是否會在對角線和斜對角線,其實滿足以下兩種性質:

  • 對角線(第一個圖):行下標-列下標=定值
  • 斜對角線(第二個圖):行下標+列下標=定值

擴散到其他左上到右下和右上到左下的對角線都有類似性質,因此我們可以直接用一個一維陣列去儲存每個線是否有皇后的情況(True/False)。

那麼為什麼要用2n呢,是因為對角線的條數是小於2n的。

所以,程式碼是這樣的:

import copy
class Solution:
    def solveNQueens(self, n: int) -> List[List[str]]:
        grid = [['.' for i in range(n)] for j in range(n)]
        res = []
        col = [False] * n
        dg = [False] * 2 * n
        xdg = [False] * 2 * n

        def dfs(u):
            r = list()
            if u == n:
                for i in range(n):
                    r.append(''.join(copy.deepcopy(grid[i])))
                res.append(r)
                return

            for i in range(n):
                if not col[i] and not dg[u+i] and not xdg[n-u+i]:
                    grid[u][i] = 'Q'
                    col[i] = dg[u+i] = xdg[n-u+i] = True
                    dfs(u+1)
                    grid[u][i] = '.'
                    col[i] = dg[u+i] = xdg[n-u+i] = False
                    
        dfs(0)
        return res

那麼N皇后II就很簡單了,直接需要返回N皇后的不同排序數量,改下返回值就OK。直接看程式碼:

class Solution:
    def totalNQueens(self, n: int) -> int:
        grid = [['.' for i in range(n)] for j in range(n)]
        self.res = 0
        col = [False] * n 
        dg = [False] * 2 * n 
        xdg = [False] * 2 * n 

        def dfs(u):
            if u == n:
                self.res += 1
                return 
            for i in range(n):
                if not col[i] and not dg[u+i] and not xdg[n-(u-i)]:
                    grid[u][i] = 'Q'
                    col[i] = dg[u+i] = xdg[n-u+i] = True
                    dfs(u + 1)
                    grid[u][i] = '.'
                    col[i] = dg[u+i] = xdg[n-u+i] = False
        dfs(0)
        return self.res 

參考連結:https://labuladong.gitbook.io/algo/di-ling-zhang-bi-du-xi-lie/hui-su-suan-fa-xiang-jie-xiu-ding-ban

https://leetcode-cn.com/problems/n-queens/solution/pythonji-chu-jie-fa-by-lockyguo-111/