LeetCode 51:N皇后问题
问题本质与挑战
N皇后问题要求在 N×N
的棋盘上放置 N
个皇后,使得它们互不攻击(同一行、列、对角线不能有多个皇后)。核心挑战:
- 组合爆炸:直接枚举所有可能(
N!
种)会超时,需通过 回溯 + 剪枝 高效搜索。 - 冲突判断:如何快速判断当前位置是否与已放置的皇后冲突。
核心思路:回溯法 + 状态剪枝
1. 状态表示:皇后位置数组
用 一维数组 queenPos
记录每行皇后的列位置:
queenPos[i]
表示第i
行皇后所在的列索引(行冲突无需检查,因为每行仅放一个皇后)。
2. 冲突判断:列与对角线
- 列冲突:当前列
col
已存在于queenPos[0..row-1]
中。 - 对角线冲突:对于已放置的皇后(第
i
行,列queenPos[i]
),行差|row-i|
等于列差|col-queenPos[i]|
(对角线斜率为±1
)。
3. 回溯流程
- 逐行放置:从第
0
行开始,尝试当前行的每一列col
。 - 冲突检查:若
col
与已放置的皇后无冲突,则放置皇后(记录queenPos[row] = col
)。 - 递归深入:处理下一行(
row+1
),直到所有行处理完毕(row == N
)。 - 回溯恢复:移除当前行的皇后(递归返回后,尝试下一列)。
算法步骤详解
步骤 1:初始化与结果集
List<List<String>> result = new ArrayList<>(); // 保存所有合法解
int[] queenPos = new int[n]; // 记录每行皇后的列位置(初始值无意义,后续覆盖)
backtrack(0, n, queenPos, result); // 从第0行开始回溯
步骤 2:回溯函数 backtrack
private void backtrack(int row, int n, int[] queenPos, List<List<String>> result) {
// 终止条件:已处理完所有行(0~n-1),找到一个合法解
if (row == n) {
result.add(convertToBoard(queenPos, n)); // 转换为棋盘字符串,加入结果
return;
}
// 遍历当前行的所有列(0 ~ n-1)
for (int col = 0; col < n; col++) {
if (isValid(row, col, queenPos)) { // 检查当前列是否可放置皇后
queenPos[row] = col; // 放置皇后
backtrack(row + 1, n, queenPos, result); // 递归处理下一行
// 回溯:无需显式恢复(下一次循环会覆盖col),但保留代码以明确逻辑
queenPos[row] = -1;
}
}
}
步骤 3:冲突检查函数 isValid
private boolean isValid(int row, int col, int[] queenPos) {
// 检查前面所有行(0 ~ row-1)的皇后
for (int i = 0; i < row; i++) {
int existingCol = queenPos[i];
// 列冲突 或 对角线冲突
if (existingCol == col || Math.abs(row - i) == Math.abs(col - existingCol)) {
return false;
}
}
return true; // 无冲突,可放置
}
步骤 4:棋盘转换函数 convertToBoard
将 queenPos
转换为字符串列表(每行用 Q
表示皇后,.
表示空位):
private List<String> convertToBoard(int[] queenPos, int n) {
List<String> board = new ArrayList<>();
for (int i = 0; i < n; i++) {
StringBuilder sb = new StringBuilder();
for (int j = 0; j < n; j++) {
if (j == queenPos[i]) { // 皇后位置
sb.append('Q');
} else { // 空位
sb.append('.');
}
}
board.add(sb.toString());
}
return board;
}
完整代码(Java)
import java.util.ArrayList;
import java.util.List;
class Solution {
public List<List<String>> solveNQueens(int n) {
List<List<String>> result = new ArrayList<>();
int[] queenPos = new int[n]; // 记录每行皇后的列位置
backtrack(0, n, queenPos, result);
return result;
}
// 回溯函数:处理第row行,尝试所有可能的列
private void backtrack(int row, int n, int[] queenPos, List<List<String>> result) {
if (row == n) { // 所有行处理完毕,生成棋盘
result.add(convertToBoard(queenPos, n));
return;
}
for (int col = 0; col < n; col++) {
if (isValid(row, col, queenPos)) { // 检查当前列是否合法
queenPos[row] = col; // 放置皇后
backtrack(row + 1, n, queenPos, result); // 递归处理下一行
queenPos[row] = -1; // 回溯(可选,下一次循环会覆盖)
}
}
}
// 检查第row行col列是否与已放置的皇后冲突
private boolean isValid(int row, int col, int[] queenPos) {
for (int i = 0; i < row; i++) {
int existingCol = queenPos[i];
// 列冲突 或 对角线冲突(行差 == 列差)
if (existingCol == col || Math.abs(row - i) == Math.abs(col - existingCol)) {
return false;
}
}
return true;
}
// 将皇后位置转换为棋盘字符串列表
private List<String> convertToBoard(int[] queenPos, int n) {
List<String> board = new ArrayList<>();
for (int i = 0; i < n; i++) {
StringBuilder sb = new StringBuilder();
for (int j = 0; j < n; j++) {
sb.append(j == queenPos[i] ? 'Q' : '.');
}
board.add(sb.toString());
}
return board;
}
}
关键逻辑解析
1. 状态表示的优化
- 用一维数组
queenPos
代替二维数组,空间复杂度从O(n²)
降为O(n)
,且通过列索引快速判断冲突。
2. 冲突判断的效率
- 仅检查前面的行(
0 ~ row-1
),因为后续行还未放置皇后,时间复杂度为O(row)
(每行最多检查row
次,总复杂度O(n²)
)。
3. 回溯的本质
- 递归调用
backtrack(row+1, ...)
时,当前行的皇后位置已确定;递归返回后,通过覆盖queenPos[row]
实现“撤销选择”,尝试下一列。
示例验证(以 n=4
为例)
- 初始状态:
row=0
,尝试col=0
,检查无冲突(前面无皇后),放置皇后(queenPos[0]=0
)。 - 递归到
row=1
:尝试col=0
(列冲突,跳过)、col=1
(对角线冲突,跳过)、col=2
(无冲突,放置queenPos[1]=2
)。 - 递归到
row=2
:尝试col=0
(列冲突)、col=1
(无冲突?检查前面行:- 行0:
col=0
→ 列差1-0=1
,行差2-0=2
→ 不冲突; - 行1:
col=2
→ 列差1-2=-1
(绝对值1),行差2-1=1
→ 对角线冲突(1==1
),故跳过。
继续尝试col=3
(无冲突,放置queenPos[2]=3
)。
- 行0:
- 递归到
row=3
:尝试col=0
(列冲突)、col=1
(无冲突?检查前面行:- 行0:
col=0
→ 列差1-0=1
,行差3-0=3
→ 不冲突; - 行1:
col=2
→ 列差1-2=-1
(绝对值1),行差3-1=2
→ 不冲突; - 行2:
col=3
→ 列差1-3=-2
(绝对值2),行差3-2=1
→ 不冲突;
放置queenPos[3]=1
,递归到row=4
(等于n=4
),生成棋盘并加入结果。
- 行0:
复杂度分析
- 时间复杂度:最坏
O(n!)
(无剪枝时),但实际因冲突检查剪枝,远低于O(n!)
(如n=4
时仅需尝试少量组合)。 - 空间复杂度:
O(n)
(queenPos
数组 + 递归栈深度n
)。
该方法通过 回溯 + 高效冲突检查,在合理时间内解决N皇后问题,是回溯算法的经典应用。核心在于状态压缩(一维数组记录位置)和剪枝策略(提前排除冲突位置),大幅减少无效搜索。