热题100 - 51. N皇后

83 阅读6分钟

题目描述:

按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。

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皇后,注意到皇后,是八路攻击的。涉及到状态还原,你思考一下。 简单想想:

  1. 需要一个棋盘,二维数组变量

  2. 遍历棋盘

  3. 对于一个棋盘位置,看它是不是点。

  4. 是点就放置Q,然后开始“攻击”

  5. “攻击”就是八路更新棋盘

  6. “攻击“之后,整理出所有的“点”。如果“点”的数量小于等待放置的皇后的数量,游戏结束了。

  7. 回溯法可以用上了,循环上一步的点,执行4,5,6,然后撤销撤销攻击,撤销点的放置

  8. 有个变量用来维护还没被放置的皇后的数量。当这个值变成0,游戏结束。记录这个位置。

  9. 可以开始写了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说应该按行来放置。一行就只能放一个,按列循环,挨个列尝试,撤回。

先按照新的理解来实现:

  1. 定义棋盘。主方法,不需要循环,直接调用dfs。原因?dfs会自己向下递归的
  2. 攻击,方向是左下,下,右下。原因?等DS解释
  3. dfs(res, board, row, n)
  4. 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了

image.png

这个实现是正确的,因为它正确地应用了回溯算法的核心思想,并妥善处理了皇后之间的冲突问题。以下是对其正确性的详细分析:

1. 核心思路

N皇后问题的关键在于确保每个皇后所在的行、列、对角线均无冲突。该代码通过逐行放置皇后(每行一个),并标记不可放置区域,确保后续行的选择不会冲突。


2. 关键逻辑解析

DFS回溯框架
  • 逐行处理:从第0行开始,每次递归处理下一行(row+1),确保每行只放一个皇后。
  • 终止条件:当处理完所有行(row >= n)且已放置n个皇后(k == 0)时,收集结果。
  • 列遍历:对当前行的每一列进行尝试,若位置合法(board[row][j] == 0),则放置皇后并递归。
冲突标记与恢复(placeQ方法)
  • 放置皇后val = 1):
    • 当前格子:标记为-1board[x][y] -= 1),表示皇后位置。
    • 下方区域:对同一列、左下方、右下方的格子增加1,标记这些位置被攻击。
  • 恢复现场val = -1):
    • 当前格子:恢复为0(board[x][y] += 1)。
    • 下方区域:对之前标记的区域减少1,撤销冲突标记。
冲突检测
  • board[i][j]的值含义
    • 0:可放置。
    • 正数:被攻击次数。
    • -1:皇后位置。
  • 合法性检查:仅当board[row][j] == 0时,允许放置皇后。

3. 正确性验证

行冲突
  • 逐行处理:每次递归固定处理下一行,天然避免行冲突。
列冲突
  • 列标记:放置皇后后,同一列下方所有格子被标记(board[i][y] += 1),后续行无法在此列放置。
对角线冲突
  • 左下方:通过循环i++, j--标记左下方对角线。
  • 右下方:通过循环i++, j++标记右下方对角线。
回溯恢复
  • 撤销操作:递归返回时,placeQ的恢复操作确保后续分支的搜索不受之前选择的影响。

4. 示例推演(n=2)

虽然n=2无解,但可验证流程:

  1. 第0行放置列0
    • 标记第0列下方(行1列0)和两个对角线(但行1列1超出范围)。
  2. 处理行1
    • 列0被标记(board[1][0] > 0),列1尝试放置,但可能被其他标记影响。
  3. 回溯恢复,尝试其他位置。

5. 总结

该实现通过逐行放置、标记冲突区域、回溯恢复,确保所有可能的合法解都被搜索到,且无重复或遗漏。其正确性依赖于:

  • 逐行处理避免行冲突。
  • 动态标记解决列和对角线冲突。
  • 回溯恢复保证状态正确重置。