题目描述:
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
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 <= n <= 9
思路:
N皇后,注意到皇后,是八路攻击的。涉及到状态还原,你思考一下。 简单想想:
-
需要一个棋盘,二维数组变量
-
遍历棋盘
-
对于一个棋盘位置,看它是不是点。
-
是点就放置Q,然后开始“攻击”
-
“攻击”就是八路更新棋盘
-
“攻击“之后,整理出所有的“点”。如果“点”的数量小于等待放置的皇后的数量,游戏结束了。
-
回溯法可以用上了,循环上一步的点,执行4,5,6,然后撤销撤销攻击,撤销点的放置
-
有个变量用来维护还没被放置的皇后的数量。当这个值变成0,游戏结束。记录这个位置。
-
可以开始写了
dfs(res, board, queueNum)
代码
class Solution {
public List<List<String>> solveNQueens(int n) {
List<List<String>> res = new ArrayList<>();
int[][] board = new int[n][n]; // -1 for Q, positive value means attacked, 0 means not used, -100 means not valid for all possiblities.
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
int size = res.size();
dfs(res, board, new int[]{i, j}, n);
board[i][j] = 0;
}
}
return res;
}
private void dfs(List<List<String>> res, int[][] board, int[] pos, int k) {
int x = pos[0];
int y = pos[1];
int n = k;
if (board[x][y] != 0) { // means this pos is not valid to put Q
return;
}
// if (detect(board, pos)) { // not a valid position to put. // maybe no need to detect.
// return;
// }
// now this pos is safe, put Q
board[x][y] = 500;
n--;
if (n == 0) { // congrats, all Q put
List<String> l1 = new ArrayList<>();
for (int i = 0; i < board.length; i++) {
StringBuilder sb = new StringBuilder();
for (int j = 0; j < board.length; j++) {
if (board[i][j] == 500) {
sb.append('Q');
} else {
sb.append('.');
}
}
l1.add(sb.toString());
}
res.add(l1);
return;
}
attack(board, pos, 2); // attack everywhere
List<int[]> newposs = tellme(board);
if (newposs.size() < n) {
attack(board, pos, -2);
board[x][y] = 0; // recover.
return;
}
for (int[] p : newposs) {
dfs(res, board, p, n);
}
attack(board, pos, -2);
board[x][y] = 0;
}
private void attack(int[][] board, int[] pos, int sign) {
// attack board, 8 directions
int x = pos[0];
int y = pos[1];
// up
int upx = x;
while (--upx >= 0) {
board[upx][y] += sign;
}
// down
int downx = x;
while (++downx < board.length) {
board[downx][y] += sign;
}
// left
int lefty = y;
while (--lefty >= 0) {
board[x][lefty] += sign;
}
// right
int righty = y;
while (++righty < board[0].length) {
board[x][righty] += sign;
}
// leftup
int lux = x;
int luy = y;
while (--lux >= 0 && --luy >= 0) {
board[lux][luy] += sign;
}
// leftdown
int ldx = x;
int ldy = y;
while (++ldx < board.length && --ldy >= 0) {
board[ldx][ldy] += sign;
}
// rightup
int rux = x;
int ruy = y;
while (--rux >= 0 && ++ruy < board[0].length) {
board[rux][ruy] += sign;
}
// rightdown
int rdx = x;
int rdy = y;
while (++rdx < board.length && ++rdy < board[0].length) {
board[rdx][rdy] += sign;
}
}
private List<int[]> tellme(int[][] board) {
int n = board.length;
List<int[]> res = new ArrayList<>();
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (board[i][j] == 0) {
res.add(new int[] {i, j});
}
}
}
return res;
}
}
但是上面的代码是错误的。错在哪里?错在攻击的方向是8向,错在对每个位置进行遍历。你问我,基于这个思路,能不能ac?答案是否定的。n >= 5就会挂掉。你问我,怎么修复?DeepSeek都想不出来。那拉到吧。重新思考。
重新思考
按点运行dfs看起来并不明智,至少在外部循环里这样做看起来并不明智。“攻击”的方法攻击了来时路,DS说应该按行来放置。一行就只能放一个,按列循环,挨个列尝试,撤回。
先按照新的理解来实现:
- 定义棋盘。主方法,不需要循环,直接调用dfs。原因?dfs会自己向下递归的
- 攻击,方向是左下,下,右下。原因?等DS解释
- dfs(res, board, row, n)
- dfs内,对列循环,若值等于0,放置Q,开始下一行的DFS,撤销放置Q。若n=0,成功了。回收放置。
代码
class Solution {
public List<List<String>> solveNQueens(int n) {
int[][] board = new int[n][n];
List<List<String>> res = new ArrayList<>();
dfs(res, board, 0, n);
return res;
}
private void dfs(List<List<String>> res, int[][] board, int row, int k) {
int n = board.length;
if (row >= n && k == 0) {
// collect data
List<String> holder = new ArrayList<>();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (board[i][j] == -1) {
sb.append('Q');
} else {
sb.append('.');
}
}
holder.add(sb.toString());
sb.setLength(0);
}
res.add(holder);
return;
}
for (int j = 0; j < n; j++) {
if (board[row][j] == 0) {
placeQ(board, new int[]{row, j}, 1);
dfs(res, board, row+1, k-1);
placeQ(board, new int[]{row, j}, -1); // recover
}
}
}
private void placeQ(int[][] board, int[] pos, int val) {
int x = pos[0], y = pos[1];
board[x][y] -= val;
for (int i = x + 1; i < board.length; i++) {
board[i][y] += val; // same column, row go down until the end.
}
for (int i = x + 1, j = y - 1; i < board.length && j >= 0; i++, j--) {
board[i][j] += val; // row go down, col go left, left down
}
for (int i = x + 1, j = y + 1; i < board.length && j < board[0].length; i++, j++) {
board[i][j] += val; // row go down, col go left, left down
}
}
}
这次ac了
这个实现是正确的,因为它正确地应用了回溯算法的核心思想,并妥善处理了皇后之间的冲突问题。以下是对其正确性的详细分析:
1. 核心思路
N皇后问题的关键在于确保每个皇后所在的行、列、对角线均无冲突。该代码通过逐行放置皇后(每行一个),并标记不可放置区域,确保后续行的选择不会冲突。
2. 关键逻辑解析
DFS回溯框架
- 逐行处理:从第0行开始,每次递归处理下一行(
row+1),确保每行只放一个皇后。 - 终止条件:当处理完所有行(
row >= n)且已放置n个皇后(k == 0)时,收集结果。 - 列遍历:对当前行的每一列进行尝试,若位置合法(
board[row][j] == 0),则放置皇后并递归。
冲突标记与恢复(placeQ方法)
- 放置皇后(
val = 1):- 当前格子:标记为
-1(board[x][y] -= 1),表示皇后位置。 - 下方区域:对同一列、左下方、右下方的格子增加1,标记这些位置被攻击。
- 当前格子:标记为
- 恢复现场(
val = -1):- 当前格子:恢复为0(
board[x][y] += 1)。 - 下方区域:对之前标记的区域减少1,撤销冲突标记。
- 当前格子:恢复为0(
冲突检测
board[i][j]的值含义:0:可放置。正数:被攻击次数。-1:皇后位置。
- 合法性检查:仅当
board[row][j] == 0时,允许放置皇后。
3. 正确性验证
行冲突
- 逐行处理:每次递归固定处理下一行,天然避免行冲突。
列冲突
- 列标记:放置皇后后,同一列下方所有格子被标记(
board[i][y] += 1),后续行无法在此列放置。
对角线冲突
- 左下方:通过循环
i++, j--标记左下方对角线。 - 右下方:通过循环
i++, j++标记右下方对角线。
回溯恢复
- 撤销操作:递归返回时,
placeQ的恢复操作确保后续分支的搜索不受之前选择的影响。
4. 示例推演(n=2)
虽然n=2无解,但可验证流程:
- 第0行放置列0:
- 标记第0列下方(行1列0)和两个对角线(但行1列1超出范围)。
- 处理行1:
- 列0被标记(
board[1][0] > 0),列1尝试放置,但可能被其他标记影响。
- 列0被标记(
- 回溯恢复,尝试其他位置。
5. 总结
该实现通过逐行放置、标记冲突区域、回溯恢复,确保所有可能的合法解都被搜索到,且无重复或遗漏。其正确性依赖于:
- 逐行处理避免行冲突。
- 动态标记解决列和对角线冲突。
- 回溯恢复保证状态正确重置。