51. N皇后#
今天要挑战的是一道算法界的”皇后级”难题 - LeetCode 51「N皇后」。这道题困扰了无数程序员,但今天我们用象棋玩家的思维,一步步将它拆解成简单易懂的小问题!
🎮 从象棋到算法#
你有没有下过国际象棋?皇后是棋盘上最强大的棋子,她可以横向、纵向、斜向任意方向移动任意步数。N皇后问题就像是在玩一局特殊的象棋:我们需要在N×N的棋盘上放置N个皇后,让她们互相都无法攻击到对方。
想象你在玩一局特殊的象棋,需要一步步地把皇后放在安全的位置上。每放置一个皇后,就会在棋盘上形成一片”危险区域”,下一个皇后就不能放在这些位置上。这不就是我们程序要解决的问题吗?
问题描述#
题目目标#
按照 n 皇后问题的规则,在 n x n 的棋盘上放置 n 个皇后,并返回所有不同的解法。每一种解法包含一个棋盘布局,其中 Q 表示皇后,. 表示空位。
示例 1#
输入: n = 4
输出: [[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
棋盘示意:
. Q . .
. . . Q
Q . . .
. . Q .text说明: 图示把输入结构和输出结果并排展开后,会更容易直接看清题目的变化过程。
补充说明#
- 棋盘中的
Q表示皇后,.表示空位。
💡 问题本质剖析#
题目要求: 在一个N×N的棋盘上放置N个皇后,使得任意两个皇后都不能互相攻击。返回所有可能的解决方案。
输入输出示例:
输入:n = 4
输出:[
[".Q..", // 解法 1
"...Q",
"Q...",
"..Q."],
["..Q.", // 解法 2
"Q...",
"...Q",
".Q.."]
]plaintext🎯 思路突破#
就像下象棋时,我们会思考:
- 每一行必须有一个皇后(因为如果同一行有两个皇后必然会互相攻击)
- 每一列必须有一个皇后(同理)
- 任意两个皇后不能在同一条斜线上
这启发我们可以:
- 逐行放置皇后
- 记录已经被攻击的列和斜线
- 用回溯法尝试所有可能的放置方案
🚀 代码实现#
class Solution {
// 主函数:返回 n 皇后的所有棋盘解法
public List<List<String>> solveNQueens(int n) {
// result:保存最终所有可行棋盘
List<List<String>> result = new ArrayList<>();
// queens[row] = col:记录第 row 行皇后放在第 col 列
int[] queens = new int[n];
// cols[col]:第 col 列是否已有皇后
boolean[] cols = new boolean[n];
// diag1[d1]:主对角线(row + col)是否已有皇后
boolean[] diag1 = new boolean[2*n-1];
// diag2[d2]:副对角线(row - col + n - 1)是否已有皇后
boolean[] diag2 = new boolean[2*n-1];
// 从第 0 行开始回溯放置皇后
backtrack(n, 0, queens, cols, diag1, diag2, result);
// 返回所有解
return result;
}
// 回溯函数:尝试在 row 行放置皇后
private void backtrack(int n, int row, int[] queens, boolean[] cols,
boolean[] diag1, boolean[] diag2, List<List<String>> result) {
// 终止条件:row == n,说明前 n 行都已成功放置皇后
if (row == n) {
// 根据 queens 生成棋盘字符串并加入结果
result.add(generateBoard(queens, n));
return;
}
// 枚举当前行的每一列,寻找可放置位置
for (int col = 0; col < n; col++) {
// 计算当前位置对应的主/副对角线索引
int d1 = row + col;
int d2 = row - col + n - 1;
// 仅当列、主对角线、副对角线都未被占用时,当前位置才安全
if (!cols[col] && !diag1[d1] && !diag2[d2]) {
// 做选择:在 (row, col) 放置皇后
queens[row] = col;
cols[col] = true;
diag1[d1] = true;
diag2[d2] = true;
// 递归处理下一行
backtrack(n, row + 1, queens, cols, diag1, diag2, result);
// 撤销选择(回溯):恢复占用状态,尝试当前行下一列
cols[col] = false;
diag1[d1] = false;
diag2[d2] = false;
}
}
}
// 根据 queens 数组生成当前解对应的棋盘字符串列表
private List<String> generateBoard(int[] queens, int n) {
// board:保存每一行的字符串表示
List<String> board = new ArrayList<>();
// 逐行构造棋盘
for (int i = 0; i < n; i++) {
// 先把该行全部初始化为 '.'(空位)
char[] row = new char[n];
Arrays.fill(row, '.');
// 在该行皇后所在列标记为 'Q'
row[queens[i]] = 'Q';
// 将字符数组转为字符串加入棋盘
board.add(new String(row));
}
// 返回构造好的棋盘
return board;
}
}java📝 解题思路详解#
让我们像下象棋一样,一步步理解这个解法:
1. 棋盘表示#
我们用一维数组 queens 记录每行皇后的列位置,这比二维数组更高效。比如 queens[2] = 3 表示第2行的皇后放在第3列。
2. 攻击区域检查#
为了快速判断一个位置是否安全,我们用三个布尔数组记录被攻击的位置:
- cols[j] 表示第j列是否有皇后
- diag1[i+j] 表示主对角线是否有皇后
- diag2[i-j+n-1] 表示副对角线是否有皇后
这就像象棋中提前计算好皇后的攻击范围!
3. 回溯过程#
就像下象棋时的思考过程:
- 在当前行找一个安全的位置放皇后
- 标记这个皇后的攻击范围
- 转移到下一行继续放置
- 如果遇到死路,就回溯到上一步重新尝试
🔍 优化技巧#
-
判断优化:
- 使用位运算代替布尔数组,可以进一步优化空间和时间
- 预先计算每个位置的攻击范围
-
空间优化:
- 只用一维数组记录皇后位置
- 用整数代替布尔数组记录攻击情况
-
回溯优化:
- 可以利用对称性减少搜索范围
- 首行皇后只需要搜索一半位置
🎯 相关题目推荐#
- N皇后 II(LeetCode 52)- 只需要计算解的数量
- 解数独(LeetCode 37)- 类似的回溯思想
- 放置盒子(LeetCode 1411)- 类似的约束放置问题
🌟 面试常见追问#
-
如何处理大规模问题?
- 可以使用并行计算
- 利用问题的对称性减少计算量
-
能否用其他算法解决?
- 可以用约束编程(Constraint Programming)
- 可以用遗传算法等启发式方法