LC 51 N 皇后:我以为难的是回溯,结果栽在了对角线下标

刷回溯算法的话,N 皇后基本是人人都会碰到的经典题。 好多教程都把它归成 "回溯入门",可我第一次做的时候,对着题目愣了快半小时。 回溯的大道理我都懂,可具体到棋盘上,怎么快速判断两个皇后会不会斜着对上? 一开始我傻乎乎写了两层循环去扫斜线,代码又臭又长,还总越界,改了三遍都没跑通。

题目说的啥

其实规则说穿了很简单。 给你一个 n×n 的棋盘,要放 n 个皇后。 皇后能攻击同行、同列、同斜线的棋子,所以摆放的时候要保证两两之间都打不到对方。 最后返回所有合法的摆法,用 Q 代表皇后,. 代表空位。

从集合到数组,效率再提一档

最开始我用三个 Set 来存占用的列和斜线,写起来倒是简单,但总觉得不够 "硬核"。 后来看到大佬们都用布尔数组来做标记,我也跟着改了一版,跑起来确实更快,也更有算法内味儿。

核心思路还是没变:按行放皇后,每行一个,天然规避行冲突。 只是把集合的哈希查询,换成了数组的下标直接访问 ------ 毕竟数组的 O (1) 才是真・常数级。

这里最绕的就是两条对角线的下标怎么算。 先说好算的:右上到左下的斜线,规律是 r + c 固定不变。 n=4 的棋盘里,r 和 c 都是 0 到 3,加起来最小是 0,最大是 6,一共 7 个不同的值。 所以数组长度开 2 * n - 1 就刚好够用,下标直接用 r + c 就行。

真正坑的是另一条,左上到右下的斜线,规律是 r - c 相等。 你算一下就会发现,这个值会出现负数。 比如 n=4,第一行最后一列,r=0,c=3,算出来是 -3。 数组下标可不能是负数啊,咋办?

很简单,给它整体加个偏移量,把所有值都掰成正数就行。 最小的负数是 -(n-1),那我们就加 n-1,这样最小值就变成 0 了。 最大值是 n-1,加完之后是 2n-2,刚好和另一条对角线的数组长度对上。 也就是最终下标 = r - c + n - 1。 我第一次写的时候偏移量算错了,写成加 n 了,结果数组越界,调了好一会儿才发现。

还有个小细节挺巧妙的。 我一开始还习惯性地用数组 push 存路径,回溯的时候再 pop。 后来发现根本不用 ------ 我们是按行递归的,第 r 行只会有一个皇后。 每次循环换列的时候,直接覆盖 queens[r] 的值就行,根本不用手动恢复。 又省了两步操作,代码也更干净。

代码实现

最终版代码长这样,逻辑紧凑,跑起来也快。 我把容易踩坑的地方都标在注释里了,都是我实打实踩过的雷。

javascript 复制代码
var solveNQueens = function(n) {
    const res = [];
    // pos[row] 表示第 row 行的皇后放在第 pos[row] 列
    const pos = Array(n).fill(0);
    // 标记列是否被占用
    const usedCol = Array(n).fill(false);
    // 标记两条对角线,长度都是 2n-1
    const antiDiag = Array(n * 2 - 1).fill(false); // 副对角线:row + col 固定
    const mainDiag = Array(n * 2 - 1).fill(false); // 主对角线:row - col + 偏移量

    function backtrack(row) {
        // 所有行都放完了,生成棋盘格式存入结果
        if (row === n) {
            res.push(pos.map(col => '.'.repeat(col) + 'Q' + '.'.repeat(n - 1 - col)));
            return;
        }

        // 尝试当前行的每一列
        for (let col = 0; col < n; col++) {
            // 主对角线的偏移下标,把负数转成正的
            const offsetIdx = row - col + n - 1;
            // 列、两条斜线都没被占用,才能放皇后
            if (!usedCol[col] && !antiDiag[row + col] && !mainDiag[offsetIdx]) {
                // 记录当前行列位置,直接覆盖,无需手动恢复现场
                pos[row] = col;
                // 标记当前列和两条斜线为已占用
                usedCol[col] = antiDiag[row + col] = mainDiag[offsetIdx] = true;
                
                // 递归处理下一行
                backtrack(row + 1);
                
                // 回溯:恢复现场
                usedCol[col] = antiDiag[row + col] = mainDiag[offsetIdx] = false;
            }
        }
    }

    backtrack(0);
    return res;
};

这里多说一句,连续赋值的写法看着简洁,但新手容易写错。 我一开始图省事,把赋值和判断写混了,结果搞出 bug,还是分开捋清楚更稳妥。

复杂度分析

时间复杂度还是 O (n!),和集合版本质一样。 毕竟核心还是回溯枚举,可选位置的数量不会因为标记方式变了就减少。 但数组访问的常数开销比集合小很多,实际运行速度会快不少。

空间复杂度是 O (n)。 三个布尔数组加起来也就 O (n) 的空间,递归栈深度也是 n,整体是线性空间。

最后唠两句

其实 N 皇后翻来覆去,核心就是回溯的框架 + 对角线的判断技巧。 从暴力检查到集合标记,再到数组下标映射,本质都是在优化 "判断冲突" 这一步的效率。 我当初绕了一大圈,最后才搞明白偏移量这点事儿,现在回头看其实就是层窗户纸。

对了,你第一次写这题的时候,是用集合还是数组? 有没有在对角线偏移量这里踩过坑? 评论区聊聊你的刷题经历,我每条都会回~

觉得有用的话,点个赞再走呀,后面还会更更多回溯经典题的踩坑笔记。

相关推荐
2401_841495642 小时前
【操作系统】进程同步与互斥实验报告
c++·算法·操作系统·进程·并发·同步·互斥
小林ixn3 小时前
从零到一理解 React 父子组件通信:手写一个 Todo 应用带你彻底搞懂单向数据流
前端·javascript·react.js
窝子面5 小时前
手搓最简前后端协作-node
javascript·数据库
爱刷碗的苏泓舒6 小时前
PPP-AR 中的参考星选取:数学原理、评价指标与切换处理
算法·gnss·模糊度固定·ppp-ar·星间单差·参考星·卫星端偏差
kyriewen6 小时前
我让AI给前端项目做了一次完整的Code Review——它和人类的差距,比我想的大得多
前端·javascript·ai编程
এ慕ོ冬℘゜6 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
sugar__salt7 小时前
Vue3 表单双向绑定与响应式核心 API 技术详解
前端·javascript·vue.js
SmartBoyW8 小时前
CSS弹性布局(Flexbox)学习笔记:从零开始搞懂弹性布局
前端·javascript
用户938515635078 小时前
写了这么久 useState,你真的知道它在干什么吗?
前端·javascript