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/