#52 回溯 困难

N 皇后 II

在 LeetCode 上查看 ↗

🎧 语音讲解

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

速度

📋 题目描述

n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n,返回 n 皇后问题 不同的解决方案的数量。

示例 1

输入:n = 4
输出:2
4 皇后问题存在两个不同的解法,如上图所示(同一行、列、斜线不能有两个皇后)。

示例 2

输入:n = 1
输出:1

💭 模拟答题者思考

1. 我先想暴力:在 n×n 的每个格子里决定放或不放皇后,共 2^{n²} 种状态,再过滤出恰好 n 个皇后且互不攻击的——思路对,但状态空间巨大。

2. 第一个剪枝:每行必须恰好一个皇后,问题变成「为每一行选一个列」,至多 n^n 种排列。

3. 重复在哪里?按行放置时,很多列选法与已有皇后同列或同斜线冲突,却还要把后面所有行试完——和 #51 N 皇后完全相同的搜索树,只是本题不要求输出棋盘。

4. 关键转化:用 colsdiag1(row-col)diag2(row+col) 三个集合 O(1) 判断冲突;DFS 逐行枚举列,能放就递归下一行,row==ncount+=1 即可,不必构造字符串棋盘。

5. 手推 n=4:搜索过程与 #51 一致,最终数到 2 种合法放置;n=2/3count=0n≤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 时说明找到一种完整解
countint定义:合法完整解的总数
维护:仅当 row==n 时加 1,无需构造棋盘字符串
更新:每到达叶子层 count += 1;DFS 结束后返回 count

⌨️ 落码步骤

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

2. 定义 DFS backtrack(row):若 row == ncount += 1 并返回

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) 启动,返回 count

💻 代码实现

class Solution:
    def totalNQueens(self, n: int) -> int:
        count = 0
        queens = [0] * n
        cols: set[int] = set()
        diag1: set[int] = set()   # row - col
        diag2: set[int] = set()   # row + col

        def backtrack(row: int) -> None:
            nonlocal count
            if row == n:
                count += 1
                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 count
class Solution {
public:
    int totalNQueens(int n) {
        int count = 0;
        vector<int> queens(n, 0);
        unordered_set<int> cols, diag1, diag2;

        function<void(int)> backtrack = [&](int row) {
            if (row == n) {
                ++count;
                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 count;
    }
};
// 时间 O(n!),空间 O(n)(递归栈 + 集合)

📈 复杂度分析

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

⚠️ 常见坑

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

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

本题只计数:到达 row==ncount+=1 即可,不要像 #51 那样构造棋盘字符串——多此一举且更慢。

🔍 必测边界 Case

Case 1:n = 1
n = 1 → 1(唯一一格放皇后,只有一种解)
Case 2:n = 2 或 3 无解
n = 2 → 0,n = 3 → 0(小棋盘不存在合法放置)
Case 3:n = 4 两解
n = 4 → 2(经典样例,与 #51 的解数一致)
Case 4:n = 9 边界
n = 9(题目上限,回溯深度 9,需依赖剪枝)