目录

题目描述

51. N 皇后

题意分析

n x n 的棋盘上摆下 n 个皇后,任意两个皇后不能互相攻击,也就是不能同行、同列、同一条斜线;要求返回所有不同的摆法,每种摆法用字符串数组描述,Q 表示皇后、. 表示空格。

注意题目要的是全部方案而非方案数,所以搜索到底后必须把整张棋盘的快照收集下来,这直接决定了收集答案时要做深拷贝。

约束信号是数量关系:棋盘 n 行,皇后也恰好 n 个,而同一行装不下两个皇后,于是每行必须且只能放一个。这条推论把「在 $n^2$ 个格子里选 $n$ 个」的组合搜索,降维成「为每一行各选一个列号」的排列搜索,搜索空间从组合数级别压到 $n!$ 以内。

另一个信号是规模:n 上限只有 9,说明出题人默认接受指数级搜索,重点考的是剪枝写得干不干净,而不是找多项式算法。

边界要留意:n = 1 时有唯一解 ["Q"]n = 2n = 3 无解,要能正常返回空列表而不是崩溃或返回半成品;斜线约束是两个方向都要管,只防一条是最典型的漏判。

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

核心思路

棋盘有 n 行且必须放 n 个皇后,同一行又不能出现两个皇后,因此每行恰好放一个。递归时直接把 row 当作决策层,每层只枚举这一行的列,同行冲突便从搜索结构中消失。

对一个候选位置 (row, col),只需判断三类冲突:

  • 列:同列皇后的 col 相同;
  • 主对角线 \:同一条线上 row - col 相同,用 row - col + n - 1 映射到非负下标;
  • 副对角线 /:同一条线上 row + col 相同。

因此用 colsdiag1diag2 三个布尔数组记录占用情况。进入 dfs(row) 时维持不变量:前 row 行各有一个互不攻击的皇后,三个数组与棋盘状态完全一致。候选位置未被占用才落子;递归返回后立即撤销棋盘和三个标记,使下一个分支从相同状态出发。

row == n,当前棋盘必然是合法完整方案。反过来,任意合法方案在每一行都有唯一列号,DFS 会沿着这组列号走到叶子且不会被错误剪掉,所以方案不漏;每条根到叶路径的列序列不同,所以方案不重。收集答案时要复制每一行,因为棋盘随后还会继续回溯修改。

解题步骤

  • 初始化 n × n 棋盘为 .,并创建长度为 n2n - 12n - 1 的列和两组对角线标记。
  • 从第 0 行开始 DFS;当前行依次尝试每一列。
  • 计算两个对角线下标。任一标记已占用,说明与前面某个皇后冲突,跳过该列。
  • 否则放置 Q 并设置三组标记,递归处理下一行。
  • 子问题返回后,恢复 . 并清除三组标记,继续尝试当前行的下一列。
  • 行号到达 n 时,把棋盘逐行转成字符串并加入答案。

n = 4 为例,列序列 [1, 3, 0, 2] 对应第一组解。放到 (0,1) 后,(1,0)row + col = 1 冲突,(1,2)row - col = -1 冲突,只有第 3 列可继续。完整搜索还会得到对称方案 [2, 0, 3, 1];从第 0 或第 3 列出发的分支都会在中途无处可放并回溯。

代码实现

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 · n!):列去重后至多枚举列排列,每个搜索节点还要扫描当前行的 n 个位置;设解的数量为 S,构造答案另需 O(S · n²)。对角线剪枝会显著减少实际搜索量。
  • 空间复杂度:不计返回结果为 O(n²),棋盘占 O(n²),三组标记和递归栈占 O(n);答案本身占 O(S · n²)

关键点总结

  • 先利用“每行恰好一个”按行建搜索树,天然消除同行冲突。
  • row - colrow + col 给两组对角线编号,把冲突检查降为 O(1)
  • 回溯状态只有棋盘和三组标记;落子与撤销必须严格对称。
  • 正确性可以从“每个合法棋盘唯一对应一组列序列”说明:DFS 枚举所有合法列序列,既不漏也不重。
  • 若题目只求方案数,可不保存棋盘;本题必须返回布局,所以叶子处需要生成快照。

易错点总结

  • 对角线公式或偏移写错(0,1)(1,2)row - col 都是 -1,应判为同一主对角线;数组下标需加 n - 1
  • 把多个标记的撤销漏掉一项:例如只清棋盘不清 diag1,会把上一分支的占用带到下一分支,导致漏解。
  • 收集可变棋盘引用:回溯结束后内容会被清空;Java 要 new String(line),Go 要把每行转成独立字符串。
  • 终止条件提前row == n - 1 时最后一行还没完成决策,只有 row == n 才是完整方案。
  • 冲突后使用 return:它会放弃当前行后续所有列;这里只应 continue
  • 只检查一组对角线:例如 (0,3)(1,2)row + col 都是 3,漏掉副对角线就会接受非法布局。

相似题目

题目 难度 考察点
52. N 皇后 II 困难 只统计方案数,可去掉棋盘并改用位运算压缩状态
面试题 08.12. 八皇后 困难 与本题同构,可用来检验模板是否真的默写熟练
37. 解数独 困难 同为约束标记加回溯,但约束按行列宫三组划分且需就地填回原盘
46. 全排列 中等 本题去掉对角线约束后的原型,只剩「列不重复」这一条
79. 单词搜索 中等 决策维度是四个方向而非一行一列,撤销的是访问标记
39. 组合总和 中等 剪枝依据是累计和而非位置冲突,且需靠起始下标去重