题目描述

✅ 51. N 皇后

image-20260928204446317

image-20260928204446318

题意分析

在 n × n 棋盘上放置恰好 n 个皇后,使任意两个皇后都不在同一行、同一列或同一条斜线上。返回全部不同的合法棋盘,用 Q 表示皇后、. 表示空位;没有解时返回空列表。

同一行最多放一个皇后,而皇后数恰好等于行数,所以每行必须放一个。这样可以把问题变成:为每一行选择一个不会与前面行冲突的列,而不必枚举任意格子组合。

解法:按行回溯 + 三组占用标记

核心思路

[!blue]

按行递归。进入 dfs(row) 时,前 row 行各有一个互不攻击的皇后,当前行及之后尚未放置。当前行只尝试每一列,行冲突由递归层次天然排除,剩下只需检查列和两组对角线。

同一列的列号相同。沿一条左上到右下的斜线,行列一起增加,row - col 不变;沿另一类斜线,行增列减,row + col 不变。因此用三组布尔标记即可在常数时间判断冲突。行列差可能为负,加上 n - 1 后,两类对角线编号都落在 [0, 2n - 2]。

若列和两条对角线都空闲,就在棋盘放入皇后并设置三组标记,再递归下一行。返回时同时撤销棋盘字符和三组标记,恢复进入当前分支前的状态,然后继续尝试本行其他列。遇到冲突只跳过当前列,不能结束整行搜索。

row == n 表示所有行都已放好,得到一组完整解。结果需要保存每一行的字符串快照,而不是继续被回溯修改的棋盘。每个合法棋盘唯一对应一组逐行列号,搜索逐层枚举这些选择,冲突才剪枝,所以不会遗漏合法解,也不会重复生成同一布局。

解题步骤

  1. 将棋盘初始化为 .,创建列标记和两组对角线标记,长度分别为 n、2n - 1、2n - 1。
  2. 从第零行开始递归。行号已到 n 时,把棋盘各行复制为字符串,收集完整方案并返回。
  3. 对当前行逐列尝试,计算列号、row - col + n - 1 和 row + col,有任一占用则跳过。
  4. 没有冲突时放入 Q、设置三个标记,递归下一行。
  5. 返回后将该格恢复为 .,清除三个标记,继续本行后面的列。所有分支结束后返回全部方案。

代码实现

class Solution {
    public List<List<String>> solveNQueens(int n) {
        List<List<String>> ans = new ArrayList<>();
        char[][] board = new char[n][n];

        for (char[] row : board) {
            Arrays.fill(row, '.');
        }

        boolean[] cols = new boolean[n];
        boolean[] diag1 = new boolean[2 * n - 1];
        boolean[] diag2 = new boolean[2 * n - 1];

        dfs(0, board, cols, diag1, diag2, ans);

        return ans;
    }

    private void dfs(
            int row,
            char[][] board,
            boolean[] cols,
            boolean[] diag1,
            boolean[] diag2,
            List<List<String>> ans) {
        int n = board.length;

        if (row == n) {
            List<String> solution = new ArrayList<>(n);

            for (char[] line : board) {
                solution.add(new String(line));
            }

            ans.add(solution);

            return;
        }

        for (int col = 0; col < n; col++) {
            // 行列差平移后与行列和分别标识两类对角线。
            int d1 = row - col + n - 1;
            int d2 = row + col;

            if (cols[col] || diag1[d1] || diag2[d2]) {
                continue;
            }

            board[row][col] = 'Q';
            cols[col] = diag1[d1] = diag2[d2] = true;
            dfs(row + 1, board, cols, diag1, diag2, ans);
            // 递归结束后同时撤销棋盘与占用,恢复下一种选择的起点。
            board[row][col] = '.';
            cols[col] = diag1[d1] = diag2[d2] = false;
        }
    }
}
func solveNQueens(n int) [][]string {
    board := make([][]byte, n)
    for i := range board {
        board[i] = make([]byte, n)
        for j := range board[i] {
            board[i][j] = '.'
        }
    }

    cols := make([]bool, n)
    diag1 := make([]bool, 2*n-1)
    diag2 := make([]bool, 2*n-1)
    ans := make([][]string, 0)

    var dfs func(int)
    dfs = func(row int) {
        if row == n {
            solution := make([]string, n)
            for i := range board {
                solution[i] = string(board[i])
            }
            ans = append(ans, solution)
            return
        }

        for col := 0; col < n; col++ {
            // 行列差平移后与行列和分别标识两类对角线。
            d1, d2 := row-col+n-1, row+col
            if cols[col] || diag1[d1] || diag2[d2] {
                continue
            }

            board[row][col] = 'Q'
            cols[col], diag1[d1], diag2[d2] = true, true, true
            dfs(row + 1)
            // 递归结束后同时撤销棋盘与占用,恢复下一种选择的起点。
            board[row][col] = '.'
            cols[col], diag1[d1], diag2[d2] = false, false, false
        }
    }

    dfs(0)
    return ans
}

复杂度分析

  • 时间复杂度:保守上界为 $O(n\cdot n!+S\cdot n^2)$,其中 S 为合法方案数。列去重后至多搜索列排列及其前缀,每个非叶搜索节点扫描 n 个位置;每个完整解还要复制 $n^2$ 个棋盘字符。对角线剪枝会减少实际搜索量。
  • 空间复杂度:不计输出为 $O(n^2)$,棋盘占 $O(n^2)$,三组标记和递归栈占 $O(n)$;全部返回棋盘另占 $O(S\cdot n^2)$。

关键点总结

[!green]

  • 每行恰好一个皇后,将格子组合转为逐行选择列,缩小搜索范围。
  • 同列、同行列差、同行列和分别对应三类需要维护的冲突。
  • 放置和撤销成对更新棋盘与标记,保证兄弟分支互不影响。
  • 本题要全部布局,找到一个解后还要继续搜索,并保存独立快照。

易错点总结

[!yellow]

  • 行列差没有平移,负数不能作为数组下标;两类对角线数组都需要覆盖 2n - 1 个编号。
  • 撤销时只改棋盘或漏掉某组标记,后续分支会继承上一分支的错误占用。
  • 只检查列或其中一组斜线,会放过另一条斜线上的攻击关系。
  • 遇到一列冲突就 return,会放弃当前行尚未尝试的其他位置;这里只能跳过该列。
  • 在 row == n - 1 时收集,最后一行还没选择,得到的是不完整布局。
  • 保存可变棋盘引用,后续回溯会改变已收集的答案;应逐行创建字符串快照。

相似题目

题目 难度 关联与区别
52. N 皇后 II 困难 搜索与约束相同,原题只计数,本题要保存每个棋盘布局。
37. 解数独 困难 同样回溯填棋盘并维护占用约束,本题按列和对角线剪枝。
转载与许可
作者
链接 https://hgnulb.github.io/blog/2026/68253214
许可 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!