📌 题目链接:51. N 皇后 - 力扣(LeetCode) 🔍 难度:困难 | 🏷️ 标签:回溯、位运算、递归、剪枝
⏱️ 目标时间复杂度:O(N!)
💾 空间复杂度:O(N)
🧠 题目分析
N 皇后问题是经典的约束满足问题(Constraint Satisfaction Problem, CSP) ,要求在 N×N 的棋盘上放置 N 个皇后,使得任意两个皇后不在同一行、同一列、或同一条对角线上。
- 每行只能放一个皇后 → 问题退化为在每行选一列放置皇后
- 每列只能有一个皇后 → 列不能重复
- 对角线不能有冲突 → 行列差/和不能重复
✅ 面试考点:
- 回溯算法模板掌握
- 对角线索引的数学建模(
row - col和row + col)- 位运算优化技巧(常用于状态压缩类回溯题)
- 剪枝思想与搜索空间压缩
🔁 核心算法及代码讲解
方法一:基于集合的回溯(清晰易懂,推荐面试写法)
我们使用三个哈希集合来记录已被占用的列和两条对角线:
columns:记录已占用的列(列索引col)diagonals1:记录“主对角线”(从左上到右下),其特征是row - col = constdiagonals2:记录“副对角线”(从右上到左下),其特征是row + col = const
📌 关键洞察:
同一条主对角线上,任意两点(r1, c1)和(r2, c2)满足r1 - c1 == r2 - c2;
同一条副对角线上,满足r1 + c1 == r2 + c2。
因此可用row - col和row + col唯一标识一条对角线!
✅ C++ 代码(带详细行注释)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
class Solution {
public:
vector<vector<string>> solveNQueens(int n) {
vector<vector<string>> solutions; // 存储所有合法解
vector<int> queens(n, -1); // queens[row] = col,表示第 row 行皇后放在 col 列
unordered_set<int> columns; // 已占用的列
unordered_set<int> diagonals1; // 主对角线:row - col
unordered_set<int> diagonals2; // 副对角线:row + col
backtrack(solutions, queens, n, 0, columns, diagonals1, diagonals2);
return solutions;
}
void backtrack(vector<vector<string>>& solutions,
vector<int>& queens,
int n,
int row,
unordered_set<int>& columns,
unordered_set<int>& diagonals1,
unordered_set<int>& diagonals2) {
// 🎯 终止条件:所有行都已放置皇后
if (row == n) {
vector<string> board = generateBoard(queens, n);
solutions.push_back(board);
return;
}
// 尝试在当前 row 行的每一列放置皇后
for (int col = 0; col < n; ++col) {
// ❌ 剪枝1:该列已被占用
if (columns.count(col)) continue;
// ❌ 剪枝2:主对角线冲突(row - col 已存在)
int d1 = row - col;
if (diagonals1.count(d1)) continue;
// ❌ 剪枝3:副对角线冲突(row + col 已存在)
int d2 = row + col;
if (diagonals2.count(d2)) continue;
// ✅ 放置皇后
queens[row] = col;
columns.insert(col);
diagonals1.insert(d1);
diagonals2.insert(d2);
// 🔁 递归处理下一行
backtrack(solutions, queens, n, row + 1, columns, diagonals1, diagonals2);
// 🔄 回溯:撤销选择(恢复现场)
queens[row] = -1;
columns.erase(col);
diagonals1.erase(d1);
diagonals2.erase(d2);
}
}
// 将 queens 数组转换为棋盘字符串表示
vector<string> generateBoard(vector<int>& queens, int n) {
vector<string> board;
for (int i = 0; i < n; ++i) {
string row(n, '.'); // 初始化全为 '.'
row[queens[i]] = 'Q'; // 在 queens[i] 列放 'Q'
board.push_back(row);
}
return board;
}
};
方法二:基于位运算的回溯(极致优化,适合进阶)
利用位掩码(bitmask) 替代集合,将空间复杂度从 O(N) 降至 O(1)(仅指状态记录部分)。
- 用一个整数
columns表示哪些列被占(第 i 位为 1 表示第 i 列不可用) diagonals1:主对角线状态,每深入一行需左移 1 位(因为对角线向右下延伸)diagonals2:副对角线状态,每深入一行需右移 1 位
📌 位运算技巧:
x & (-x):提取最低位的 1(如010100→000100)x & (x - 1):清除最低位的 1(用于遍历所有可选位置)__builtin_ctz(x):返回 x 的二进制末尾 0 的个数(即最低位 1 的位置)
✅ C++ 位运算版(行注释)
void solve(vector<vector<string>>& solutions,
vector<int>& queens,
int n,
int row,
int columns,
int diagonals1,
int diagonals2) {
if (row == n) {
solutions.push_back(generateBoard(queens, n));
return;
}
// 计算当前行所有可放置的位置(1 表示可用)
// (1 << n) - 1 得到低 n 位全 1 的掩码
// ~(columns | d1 | d2) 取反得到可用位
// & ((1 << n) - 1) 确保只保留低 n 位
int available = ((1 << n) - 1) & (~(columns | diagonals1 | diagonals2));
while (available) {
int pos = available & (-available); // 取最低位的 1
available &= (available - 1); // 清除该位
int col = __builtin_ctz(pos); // 获取列索引(从 0 开始)
queens[row] = col;
// 递归:columns | pos 标记该列占用
// diagonals1 | pos 后右移(注意:代码中是 >>1,因为下一行时对角线右移)
// diagonals2 | pos 后左移
solve(solutions, queens, n, row + 1,
columns | pos,
(diagonals1 | pos) >> 1,
(diagonals2 | pos) << 1);
queens[row] = -1; // 回溯(此处其实可省略,因会被覆盖)
}
}
💡 注意:位运算版本中,
diagonals1和diagonals2在递归调用时分别 右移和左移,是因为:
- 主对角线(\):下一行时,冲突位置向右移动 → 位掩码需右移(
>>1)- 副对角线(/):下一行时,冲突位置向左移动 → 位掩码需左移(
<<1)
🧩 解题思路(分步拆解)
-
问题建模
- 每行放一个皇后 → 只需决定每行的列位置
- 用数组
queens[row] = col记录方案
-
冲突检测
- 列冲突:
col是否已用? - 主对角线冲突:
row - col是否已存在? - 副对角线冲突:
row + col是否已存在?
- 列冲突:
-
回溯框架
backtrack(row): if row == n: 保存解 for col in 0..n-1: if col / d1 / d2 冲突: continue 选择 col backtrack(row + 1) 撤销选择 -
结果生成
- 遍历
queens数组,按列位置生成"....Q..."字符串
- 遍历
📊 算法分析
| 项目 | 分析 |
|---|---|
| 时间复杂度 | O(N!) —— 最坏情况下需遍历所有排列,但剪枝大幅减少实际搜索节点 |
| 空间复杂度 | O(N) —— 递归深度为 N,queens 数组和集合/位掩码均为 O(N) |
| 剪枝效果 | 通过列和对角线约束,避免无效搜索,实际运行远快于理论最坏情况 |
| 适用场景 | 所有“在网格中放置互斥元素”的问题(如数独、八数码等) |
🎯 面试加分点:
- 能解释为什么
row - col和row + col能唯一标识对角线- 能对比集合 vs 位运算的优劣(可读性 vs 性能)
- 能指出 N 皇后问题在 N=2,3 时无解(可作为边界测试)
💻 完整代码
C++(集合回溯版 —— 推荐面试使用)
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
class Solution {
public:
vector<vector<string>> solveNQueens(int n) {
vector<vector<string>> solutions;
vector<int> queens(n, -1);
unordered_set<int> columns;
unordered_set<int> diagonals1;
unordered_set<int> diagonals2;
backtrack(solutions, queens, n, 0, columns, diagonals1, diagonals2);
return solutions;
}
void backtrack(vector<vector<string>>& solutions,
vector<int>& queens,
int n,
int row,
unordered_set<int>& columns,
unordered_set<int>& diagonals1,
unordered_set<int>& diagonals2) {
if (row == n) {
vector<string> board = generateBoard(queens, n);
solutions.push_back(board);
return;
}
for (int col = 0; col < n; ++col) {
if (columns.count(col)) continue;
int d1 = row - col;
if (diagonals1.count(d1)) continue;
int d2 = row + col;
if (diagonals2.count(d2)) continue;
queens[row] = col;
columns.insert(col);
diagonals1.insert(d1);
diagonals2.insert(d2);
backtrack(solutions, queens, n, row + 1, columns, diagonals1, diagonals2);
queens[row] = -1;
columns.erase(col);
diagonals1.erase(d1);
diagonals2.erase(d2);
}
}
vector<string> generateBoard(vector<int>& queens, int n) {
vector<string> board;
for (int i = 0; i < n; ++i) {
string row(n, '.');
row[queens[i]] = 'Q';
board.push_back(row);
}
return board;
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
Solution sol;
auto res1 = sol.solveNQueens(4);
// 输出: [[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
for (auto& board : res1) {
for (string& row : board) {
cout << row << "\n";
}
cout << "-----\n";
}
auto res2 = sol.solveNQueens(1);
// 输出: [["Q"]]
for (auto& row : res2[0]) {
cout << row << "\n";
}
return 0;
}
JavaScript(集合回溯版)
/**
* @param {number} n
* @return {string[][]}
*/
var solveNQueens = function(n) {
const solutions = [];
const queens = new Array(n).fill(-1);
const columns = new Set();
const diagonals1 = new Set(); // row - col
const diagonals2 = new Set(); // row + col
const backtrack = (row) => {
if (row === n) {
solutions.push(generateBoard(queens, n));
return;
}
for (let col = 0; col < n; col++) {
if (columns.has(col)) continue;
const d1 = row - col;
if (diagonals1.has(d1)) continue;
const d2 = row + col;
if (diagonals2.has(d2)) continue;
queens[row] = col;
columns.add(col);
diagonals1.add(d1);
diagonals2.add(d2);
backtrack(row + 1);
queens[row] = -1;
columns.delete(col);
diagonals1.delete(d1);
diagonals2.delete(d2);
}
};
const generateBoard = (queens, n) => {
const board = [];
for (let i = 0; i < n; i++) {
let row = '.'.repeat(n);
row = row.substring(0, queens[i]) + 'Q' + row.substring(queens[i] + 1);
board.push(row);
}
return board;
};
backtrack(0);
return solutions;
};
// 测试
console.log(solveNQueens(4));
console.log(solveNQueens(1));
🧪 测试用例 & 结果
输入:n = 4
输出:
[".Q..", "...Q", "Q...", "..Q."]
-----
["..Q.", "Q...", "...Q", ".Q.."]
输入:n = 1
输出:["Q"]
边界测试:
n = 2→[](无解)n = 3→[](无解)n = 9→ 返回 352 种解(验证正确性)
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!