深度解析:LeetCode 51. N 皇后 —— 回溯算法的巅峰之作

N 皇后问题是算法面试中的"常客",也是理解回溯算法(Backtracking)最经典的题目。很多初学者看到这道题,第一反应是"暴力穷举",但很快就会因为超时而放弃。其实,只要掌握了 "逐行放置""对角线数学映射" 这两个核心技巧,这道 Hard 级别的题目就能迎刃而解。

一、 题目核心与约束条件

题目要求在一个 n × n 的棋盘上放置 n 个皇后,使得它们彼此不能相互攻击。皇后的攻击范围极广,涵盖了:

  1. 同一行
  2. 同一列
  3. 同一主对角线(左上到右下)
  4. 同一副对角线(右上到左下)

二、 核心思路:降维打击与空间换时间

如果我们在 n × n 的网格里盲目尝试每个格子,时间复杂度会高得离谱。但我们可以利用规则进行降维

既然任意两个皇后不能同行,那么每一行必然且只能放置一个皇后

这样一来,我们就不需要遍历二维网格了,只需要逐行递归 。在第 row 行,我们只需要尝试 0n-1 列,检查当前位置是否安全即可。

为了保证 O(1) 的时间复杂度判断冲突,我们需要用三个布尔数组来记录"攻击范围":

  • cols[col]:记录第 col 列是否被占用。
  • diag1[row - col]:记录主对角线是否被占用。
  • diag2[row + col]:记录副对角线是否被占用。

三、 难点深度解析:对角线的数学映射

这是本题最劝退小白的地方,但其实背后的数学逻辑非常优美:

  1. 副对角线()
    观察棋盘,同一条副对角线上的格子,其 行号 + 列号 的值是恒定的。
    例如:(0, 3), (1, 2), (2, 1), (3, 0),它们的 row + col 都等于 3。
    所以,我们可以直接用 row + col 作为副对角线的唯一索引。
  2. 主对角线()
    同一条主对角线上的格子,其 行号 - 列号 的值是恒定的。
    例如:(0, 0), (1, 1), (2, 2),它们的 row - col 都等于 0。
    注意: row - col 可能会出现负数(比如 0 - 3 = -3),数组下标不能为负。因此,我们加上一个偏移量 n - 1,将其映射到正数区间。最终索引为 row - col + n - 1

四、 完整代码实现(JavaScript)

理解了上述逻辑,代码其实就是"做选择 -> 递归 -> 撤销选择"的标准回溯模板:

ini 复制代码
/**
 * @param {number} n
 * @return {string[][]}
 */
var solveNQueens = function(n) {
    const res = [];
    // 初始化棋盘,全部填充为 '.'
    const board = Array.from({ length: n }, () => Array(n).fill('.'));
    
    // 记录列、主对角线、副对角线的占用情况
    const cols = new Array(n).fill(false);
    const diag1 = new Array(2 * n - 1).fill(false); // 主对角线
    const diag2 = new Array(2 * n - 1).fill(false); // 副对角线

    // 回溯函数:从第 row 行开始放置皇后
    const backtrack = (row) => {
        // 终止条件:所有行都成功放置了皇后
        if (row === n) {
            // 将二维字符数组转换为字符串数组,加入结果集
            res.push(board.map(row => row.join('')));
            return;
        }

        // 在当前行的每一列尝试放置
        for (let col = 0; col < n; col++) {
            // 计算两条对角线的索引
            const d1 = row - col + n - 1; // 主对角线
            const d2 = row + col;         // 副对角线

            // 剪枝:如果当前列或对角线已被占用,直接跳过
            if (cols[col] || diag1[d1] || diag2[d2]) {
                continue;
            }

            // 1. 做选择:放置皇后,并标记占用
            board[row][col] = 'Q';
            cols[col] = true;
            diag1[d1] = true;
            diag2[d2] = true;

            // 2. 递归:进入下一行
            backtrack(row + 1);

            // 3. 撤销选择(回溯):拿走皇后,恢复未占用状态
            board[row][col] = '.';
            cols[col] = false;
            diag1[d1] = false;
            diag2[d2] = false;
        }
    };

    // 从第 0 行开始回溯
    backtrack(0);
    return res;
};

五、 复杂度分析

  • 时间复杂度:O(N!)。第一行有 N 种选择,第二行最多有 N-1 种选择,依此类推。虽然实际运行中剪枝会跳过大量无效分支,但最坏情况下依然是阶乘级别。
  • 空间复杂度:O(N)。主要是递归调用栈的深度(N 层)以及三个辅助数组的空间(O(N) + O(2N-1) + O(2N-1)),忽略存储结果的空间,额外空间为 O(N)。

六、 总结

N 皇后问题看似复杂,实则是对回溯思想数学映射能力 的双重考察。只要记住"逐行放置"的降维策略,以及 row+colrow-col+n-1 这两个对角线公式,你就能轻松写出优雅的解法。建议大家在看完解析后,自己动手敲一遍代码,感受"做选择"与"撤销选择"的对称美。

相关推荐
咪饭只吃一小碗1 小时前
# C++ STL 常用容器方法全面总结vector / map / set / unordered_map / unordered_set
算法
daad7771 小时前
802.11 前导码与 STF 深度解析(含检测算法与 5G 对比
人工智能·算法·5g·wifi·802.11·前导码
Navigator_Z2 小时前
LeetCode //C - 1203. Sort Items by Groups Respecting Dependencies
c语言·算法·leetcode
我变成萤火虫3 小时前
2026 ICPC沈阳邀请赛Vp补题
数据结构·c++·算法·贪心算法·stl·排序算法·动态规划
风起洛阳@不良使3 小时前
中级软考(软件攻城狮)第3章知识点——数据结构与数据运算(线性结构+非线性结构)
数据结构·算法·链表
AndrewHZ3 小时前
图像处理入门009 | OpenCV 图像读取与显示:imread/imshow 全解析
图像处理·python·opencv·算法·计算机视觉·图像显示
学计算机的计算基3 小时前
TCP 传输层硬核整理:三次握手、四次挥手、拥塞控制一次讲透
java·网络·笔记·网络协议·算法
xx~t4 小时前
嵌入式学习22
数据结构·学习·算法·排序算法