刷回溯算法的话,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 皇后翻来覆去,核心就是回溯的框架 + 对角线的判断技巧。 从暴力检查到集合标记,再到数组下标映射,本质都是在优化 "判断冲突" 这一步的效率。 我当初绕了一大圈,最后才搞明白偏移量这点事儿,现在回头看其实就是层窗户纸。
对了,你第一次写这题的时候,是用集合还是数组? 有没有在对角线偏移量这里踩过坑? 评论区聊聊你的刷题经历,我每条都会回~
觉得有用的话,点个赞再走呀,后面还会更更多回溯经典题的踩坑笔记。