LeetCode 面试题 08.12. 八皇后
题目描述

题意分析
在
n×n棋盘上放n个互不攻击的皇后,返回全部布局。皇后不能同行、同列,也不能处于任意一条对角线上;题目名称是八皇后,实际棋盘大小由n决定。
n个皇后分布在n行且不能同行,所以每行恰好放一个。搜索只需逐行选择列,不必枚举任意格子组合。
解法:按行回溯 + 三组占用标记
核心思路
[!blue]
用
dfs(row)为第row行选择皇后的位置。进入这一层时,前row行各有一个皇后且彼此不冲突,当前行及之后都为空;三个占用数组只记录这些已放皇后。同一条主对角线上的格子满足
row-col相等,同一条副对角线满足row+col相等。前者加上n-1后,两类编号都落在[0, 2n-2],因此各用长度2n-1的数组即可。候选位置的列和两个对角线都未占用时,放下皇后不会与前面的皇后冲突,能够继续处理下一行。每次递归返回,都撤销本次放置及三个标记,恢复进入该分支之前的状态,再尝试下一列。到达
row == n时,恰好放完n个互不攻击的皇后,将棋盘转为字符串快照保存。每个合法布局在每一行都有唯一的列选择,这条路径不会被冲突检查剪掉,因此不会漏解;不同选择路径至少有一行不同,也不会重复收集。某行无处可放时直接返回,由上一行改选位置。
解题步骤
- 初始化全为
.的棋盘,以及列、主对角线、副对角线三个占用数组。- 在第
row行依次尝试每个col,计算两个对角线编号。- 任一编号已占用就剪枝;否则放置
Q并设置三组标记。- 递归处理下一行,返回后撤销棋盘和全部标记。
row == n时复制棋盘到答案。
n = 1时直接得到一个布局;n = 2或n = 3时所有分支都会遇到冲突,答案自然为空,不需要特殊处理。
代码实现
class Solution {
public List<List<String>> solveNQueens(int n) {
char[][] board = new char[n][n];
for (char[] row : board) {
Arrays.fill(row, '.');
}
List<List<String>> answer = new ArrayList<>();
dfs(0, board, new boolean[n], new boolean[2 * n - 1], new boolean[2 * n - 1], answer);
return answer;
}
private void dfs(
int row,
char[][] board,
boolean[] columns,
boolean[] diagonals,
boolean[] antiDiagonals,
List<List<String>> answer) {
int n = board.length;
if (row == n) {
List<String> solution = new ArrayList<>(n);
for (char[] line : board) {
solution.add(new String(line));
}
answer.add(solution);
return;
}
for (int col = 0; col < n; col++) {
// 行减列可能为负,平移后作为同一主对角线的编号。
int diagonal = row - col + n - 1;
int antiDiagonal = row + col;
if (columns[col] || diagonals[diagonal] || antiDiagonals[antiDiagonal]) {
continue;
}
board[row][col] = 'Q';
columns[col] = true;
diagonals[diagonal] = true;
antiDiagonals[antiDiagonal] = true;
dfs(row + 1, board, columns, diagonals, antiDiagonals, answer);
// 递归结束后同时撤销棋盘与三类占用,恢复本行选择前的状态。
board[row][col] = '.';
columns[col] = false;
diagonals[diagonal] = false;
antiDiagonals[antiDiagonal] = false;
}
}
}
func solveNQueens(n int) [][]string {
board := make([][]byte, n)
for row := range board {
board[row] = make([]byte, n)
for col := range board[row] {
board[row][col] = '.'
}
}
answer := make([][]string, 0)
columns := make([]bool, n)
diagonals := make([]bool, 2*n-1)
antiDiagonals := make([]bool, 2*n-1)
var dfs func(int)
dfs = func(row int) {
if row == n {
solution := make([]string, n)
for i := range board {
solution[i] = string(board[i])
}
answer = append(answer, solution)
return
}
for col := 0; col < n; col++ {
// 行减列可能为负,平移后作为同一主对角线的编号。
diagonal := row - col + n - 1
antiDiagonal := row + col
if columns[col] || diagonals[diagonal] || antiDiagonals[antiDiagonal] {
continue
}
board[row][col] = 'Q'
columns[col] = true
diagonals[diagonal] = true
antiDiagonals[antiDiagonal] = true
dfs(row + 1)
board[row][col] = '.'
// 棋盘已恢复,三类占用也要一起撤销,才能尝试下一列。
columns[col] = false
diagonals[diagonal] = false
antiDiagonals[antiDiagonal] = false
}
}
dfs(0)
return answer
}
复杂度分析
- 时间复杂度:上界为 $O(n \cdot n! + S \cdot n^2)$,其中 $S$ 是解的数量。列不能重复,搜索节点数受列排列数量限制;每个节点仍扫描
n列,每个完整布局还需复制n²个字符。- 空间复杂度:$O(n^2)$,棋盘占主要空间,标记数组和递归深度为 $O(n)$;返回结果不计入额外空间。
关键点总结
[!green]
- 逐行放置保证行不冲突,列与两类对角线标记负责剩下的约束。
- 标记始终与当前棋盘一致,放置和撤销必须成对进行。
- 找到的是全部布局,收集一个答案后仍须回溯继续搜索。
易错点总结
[!yellow]
- 对角线标记长度为
2n-1,且row-col必须平移n-1,否则可能越界。- 必须在
row == n时记录,此时最后一行已经放完。- 不能把可变棋盘直接加入答案;后续撤销会改变它,必须保存字符串快照。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 52. N 皇后 II | 困难 | 搜索与约束相同,原题只计数,本题要保存每个棋盘布局。 |
| 37. 解数独 | 困难 | 同样回溯填棋盘并维护占用约束,本题按列和对角线剪枝。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!