一、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”]]
二、解题思路
1.回溯法
用 DFS 的回溯法求解从树的根节点 [][] 开始按照深度逐个遍历
回溯法算实际上就是一个决策树(因为在每个节点上都要做决策)的遍历过程:
路径:也就是已经做出的选择。
选择列表:也就是当前可以做的选择。
结束条件:也就是到达决策树底层(选择列表为空的时候),无法再做出选择的条件。
与全排列问题差不多,决策树的每一层表示棋盘上的每一行;每个节点可以做出的选择是,在该行任意一列放置一个皇后。
首先,创建棋盘,其中 ’ . ’ 表示空,’ Q ’ 表示皇后,初始化空棋盘;
然后,调用 backtrack()backtrack() 方法:
“路径”:boardboard 中小于 rowrow 的那些行都已经成功放置了皇后;
“选择列表”:第 rowrow 行的所有列都是放置皇后的选择;
“结束条件”:rowrow 超过 boardboard 的最后一行,说明棋盘放满了。
关键代码
class Solution {
List<List<String>> res=new ArrayList<>();
public List<List<String>> solveNQueens(int n) {
char[][] board=new char[n][n];// 定义棋盘大小,并初始化棋盘
for (char[] c : board) Arrays.fill(c, '.');
backtrack(n, board, 0);
return res;
}
// 是否可以在 board[row][col] 位置放置皇后
public boolean isValid(char[][] board, int row, int col, int n) {
// 检查列中是否有皇后互相冲突
for (int i=0;i<row;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;
}
// 将 char[][] 中的每一行转换成 List 集合
public List<String> Array2List(char[][] board) {
List<String> list = new ArrayList<>();
for (char[] c : board) {
list.add(String.copyValueOf(c));
}
return list;
}
public void backtrack(int n, char[][] board, int row) {
if (row == n) {
res.add(Array2List(board));
return ;
}
for (int col=0;col<n;col++) {
if (!isValid(board, row, col, n)) continue; // 剪枝
board[row][col] = 'Q'; // 做选择
backtrack(n, board, row + 1);
board[row][col] = '.'; // 撤销选择
}
}
}
剪枝
要判断是否可以在 board[row][col]board[row][col] 位置放置皇后:因为皇后是从上往下一行一行放置的,所以只需要检查正上方、左上方、右上方三个方向,而不用检查当前行和下面的三个方向。
若在需要检查的三个方向上存在 board[i][j] = ‘Q’board[i][j]= ‘Q’
,则说明在 board[row][col]board[row][col] 位置上不能够放置皇后,此时直接跳过该位置,继续遍历下一个位置即可。
// 是否可以在 board[row][col] 位置放置皇后
public boolean isValid(char[][] board, int row, int col, int n) {
// 检查列中是否有皇后互相冲突
for (int i=0;i<row;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;
}
if (!isValid(board, row, col, n)) continue; // 剪枝
当在 “做选择” 时,就是从 “选择列表” 中拿出一个 “选择”,并将它放入到 “路径” 中。
若当前位置 board[row][col]board[row][col] 可以放置皇后,则令 board[row][col] = 'Q’board[row][col]=
′
Q
′
,并且在调用 backtrack()backtrack() 方法时,传入的参数中的行数应该 + 1+1,即 row + 1row+1 以进行下一行的决策。
结束条件:若当前传入的参数 row == nrow==n,说明已经遍历完棋盘 boardboard 的最后一行了,此时棋盘已经放满了,需要先将当前棋盘 boardboard 中的每一行都都转换成 StringString 类型的 listlist 集合,然后将该集合添加到 resres 中并回溯到上一层,将上一层 borad[row][col]borad[row][col] 位置上的皇后拿掉,即 board[row][col] = '.'board[row][col]= ’,‘。
(回溯到上一层后,继续遍历选择列表,选择下一个分支)(“撤销选择”:就是从 “路径” 中拿出一个选择,将它恢复到 “选择列表” 中)。