#51 回溯 困难

N 皇后

在 LeetCode 上查看 ↗

🎧 语音讲解

开车或通勤时可听,跟着思路走一遍

速度

📋 题目描述

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

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×n 的每个格子里决定放或不放皇后,共 2^{n²} 种状态,再过滤出恰好 n 个皇后且互不攻击的——思路对,但状态空间巨大。

2. 第一个剪枝:每行必须恰好一个皇后(否则某行空着或有两个,都不合法),问题变成「为每一行选一个列」,至多 n^n 种排列。

3. 重复在哪里?按行放置时,子问题变成「前 row 行已放好,第 row 行该放哪一列?」——很多列选法会与已有皇后同列或同斜线冲突,却还要把后面所有行试完。

4. 关键转化:用 colsdiag1(row-col)diag2(row+col) 三个集合 O(1) 判断冲突;DFS 逐行枚举列,能放就递归下一行,子树走不通立刻撤销换列。

5. 手推 n=4:第 0 行试 col=0 会一路走到死路;col=1 得解 ".Q.."/"...Q"/"Q..."/"..Q.";继续搜索还能找到对称解。n≤9,回溯深度 ≤ 9,完全可行。

🧠 变量语义(先读这三句再编码)

变量类型语义(三句法)
queensint[n]定义queens[row] 表示第 row 行皇后所在的列号
维护:每行恰好放一个皇后,用一维数组即可完整描述当前部分解
更新:在 row 行尝试列 col 时令 queens[row]=col;回溯返回后该位置会被下一列覆盖
colsset<int>定义:已被占用的列号集合
维护:任意时刻,已放置的皇后两两不同列
更新:在 (row,col) 放皇后前查 col not in cols;放入时 add(col),回溯时 remove(col)
diag1set<int>定义:主对角线标识 row - col 的已占用集合(↘ 方向同线相等)
维护:同一主对角线上任意两格 row-col 相同
更新:放皇后前查 (row-col) not in diag1;放入/撤销与 cols 同步
diag2set<int>定义:副对角线标识 row + col 的已占用集合(↗ 方向同线相等)
维护:同一副对角线上任意两格 row+col 相同
更新:放皇后前查 (row+col) not in diag2;放入/撤销与 cols 同步
rowint定义:当前待放置皇后的行号(从 0 到 n-1)
维护:DFS 逐行向下推进,每行只尝试合法列
更新:初始为 0;每成功放一行后 row+1 递归;row==n 时收集完整解
anslist<list<str>>定义:所有合法棋盘的字符串表示
维护:仅当 row==n 时,按 queens 构造 n 行字符串并加入
更新:每到达叶子层追加一次;构造时第 r 行在 queens[r] 处放 'Q',其余为 '.'

⌨️ 落码步骤

1. 初始化 ans = []queens = [0]*n,以及空集合 cols, diag1, diag2

2. 定义 DFS backtrack(row):若 row == n,按 queens 构造 n 行字符串加入 ans 并返回

3. 对 col 从 0 到 n-1:若 col in cols(row-col) in diag1(row+col) in diag2,跳过

4. 否则:登记三个集合、queens[row]=col,递归 backtrack(row+1)

5. 回溯:从三个集合中移除本次登记,继续尝试下一列

6. 从 backtrack(0) 启动,返回 ans

💻 代码实现

class Solution:
    def solveNQueens(self, n: int) -> List[List[str]]:
        ans: list[list[str]] = []
        queens = [0] * n
        cols: set[int] = set()
        diag1: set[int] = set()   # row - col
        diag2: set[int] = set()   # row + col

        def backtrack(row: int) -> None:
            if row == n:
                board = []
                for r in range(n):
                    line = ['.'] * n
                    line[queens[r]] = 'Q'
                    board.append(''.join(line))
                ans.append(board)
                return
            for col in range(n):
                if col in cols or (row - col) in diag1 or (row + col) in diag2:
                    continue
                cols.add(col)
                diag1.add(row - col)
                diag2.add(row + col)
                queens[row] = col
                backtrack(row + 1)
                cols.remove(col)
                diag1.remove(row - col)
                diag2.remove(row + col)

        backtrack(0)
        return ans
class Solution {
public:
    vector<vector<string>> solveNQueens(int n) {
        vector<vector<string>> ans;
        vector<int> queens(n, 0);
        unordered_set<int> cols, diag1, diag2;

        function<void(int)> backtrack = [&](int row) {
            if (row == n) {
                vector<string> board(n, string(n, '.'));
                for (int r = 0; r < n; ++r)
                    board[r][queens[r]] = 'Q';
                ans.push_back(move(board));
                return;
            }
            for (int col = 0; col < n; ++col) {
                if (cols.count(col) || diag1.count(row - col) || diag2.count(row + col))
                    continue;
                cols.insert(col);
                diag1.insert(row - col);
                diag2.insert(row + col);
                queens[row] = col;
                backtrack(row + 1);
                cols.erase(col);
                diag1.erase(row - col);
                diag2.erase(row + col);
            }
        };

        backtrack(0);
        return ans;
    }
};
// 时间 O(n!),空间 O(n)(递归栈 + 集合,不计输出)

📈 复杂度分析

时间复杂度 O(n!)(剪枝后远好于 n^n 全枚举)
空间复杂度 O(n)(递归栈 + 冲突集合,不计输出)

⚠️ 常见坑

回溯不撤销:放入皇后后递归返回,必须从 cols/diag1/diag2 中移除本次登记,否则污染兄弟分支。

斜线判断写错:主对角线是 row - col 相同,副对角线是 row + col 相同;不要混用或漏判其中一种。

输出格式:每行是长度为 n 的字符串,'Q''.' 组成;不是坐标列表,也不是二维字符数组的嵌套列表混用 int。

🔍 必测边界 Case

Case 1:n = 1
n = 1 → [["Q"]](唯一一格放皇后)
Case 2:n = 2 或 3 无解
n = 2 → [],n = 3 → [](小棋盘不存在合法放置,应返回空列表)
Case 3:n = 4 两解
n = 4 → 2 种棋盘(经典样例,注意两种解互为镜像/旋转)
Case 4:n = 9 边界
n = 9(题目上限,回溯深度 9,需依赖剪枝)