今天我们来死磕一道回溯算法里大名鼎鼎的"巅峰之作"------N皇后问题。
很多人一听到"N皇后"就觉得高深莫测,但如果你跟着"代码随想录"的思路,把树形结构画出来,就会发现它其实就是一个标准的回溯模板题。
今天这篇文章,我们将从核心逻辑拆解,到 isValid 函数的细节剖析,再到多语言(C, C++, Python)的代码实现,最后附带新手高频踩坑点总结,帮你彻底拿下这道题!
一、 核心思路:回溯三部曲
N皇后问题的本质是:在一个N * N 的棋盘上放置 N 个皇后,使得她们不能互相攻击(不能同行、同列、同对角线)。
我们采取的策略是按行放置:从第 0 行开始,逐行往下放。这样我们就天然地避开了"同行"的冲突。
在树形结构中:
-
树的深度(递归) :代表棋盘的行 (
row)。 -
树的宽度(for循环) :代表棋盘的列 (
col)。
1. 递归函数参数
我们需要传入当前棋盘的大小 n,当前处理到的行数 row,以及当前的棋盘状态 chessboard。
2. 终止条件
当我们走到叶子节点,也就是 row == n 时,说明 N 个皇后都已经成功放置完毕。此时将当前棋盘状态加入结果集 res,然后 return。
3. 单层搜索逻辑
在当前行 row,我们用一个 for 循环遍历所有的列 col。
如果当前位置 (row, col) 可以放皇后(通过 isValid 验证):
-
放置皇后:
chessboard[row][col] = 'Q' -
递归进入下一行:
backtracking(row + 1) -
回溯(撤销操作) :
chessboard[row][col] = '.'
偷一下卡哥的图

二、 灵魂拷问:isValid 验证函数怎么写?
在放置皇后之前,必须检查当前位置是否安全。因为我们是从上往下,一行一行 放的,当前行 row 的下方全是空的。
所以,我们只需要检查三个方向(也就是向上看的三个方向):
-
正上方:同一列是否有皇后?
-
左上对角线:45度角是否有皇后?
-
右上对角线:135度角是否有皇后?
(注:不需要检查同一行,因为 for 循环每次只会在当前行选一个位置放;也不需要检查下方,因为还没走到那里。)
三、 多语言代码实现
1. Python 3 实现(附带切片技巧)
Python 中字符串是不可变的,所以修改棋盘某一个字符时,需要用到字符串切片拼接。
python
from typing import List
class Solution:
def solveNQueens(self, n: int) -> List[List[str]]:
res = []
# 初始化空棋盘
qipan = ["." * n for _ in range(n)]
def backtracking(row):
# 终止条件:走完最后一行
if row == n:
res.append(qipan[:])
return
# 遍历当前行的每一列
for col in range(n):
if isValid(row, col):
# 放置皇后 (注意:修改的是当前行 qipan[row])
qipan[row] = qipan[row][:col] + "Q" + qipan[row][col + 1:]
backtracking(row + 1)
# 回溯:撤销皇后
qipan[row] = qipan[row][:col] + "." + qipan[row][col + 1:]
def isValid(row, col):
# 1. 检查正上方
for i in range(row):
if qipan[i][col] == "Q": return False
# 2. 检查左上方对角线
i, j = row - 1, col - 1
while i >= 0 and j >= 0:
if qipan[i][j] == "Q": return False
i -= 1; j -= 1
# 3. 检查右上方对角线
i, j = row - 1, col + 1
while i >= 0 and j < n:
if qipan[i][j] == "Q": return False
i -= 1; j += 1
# 三个方向都安全,返回 True
return True
backtracking(0)
return res
2. C++ 实现 (代码随想录经典版)
C++ 的 std::string 是可变的,直接用索引修改即可,代码非常清爽。
cpp
#include <vector>
#include <string>
using namespace std;
class Solution {
private:
vector<vector<string>> result;
void backtracking(int n, int row, vector<string>& chessboard) {
if (row == n) {
result.push_back(chessboard);
return;
}
for (int col = 0; col < n; col++) {
if (isValid(row, col, chessboard, n)) {
chessboard[row][col] = 'Q'; // 放置皇后
backtracking(n, row + 1, chessboard); // 递归
chessboard[row][col] = '.'; // 回溯,撤销皇后
}
}
}
bool isValid(int row, int col, vector<string>& chessboard, int n) {
// 检查列
for (int i = 0; i < row; i++) {
if (chessboard[i][col] == 'Q') return false;
}
// 检查左上对角线 (45度)
for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
if (chessboard[i][j] == 'Q') return false;
}
// 检查右上对角线 (135度)
for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
if (chessboard[i][j] == 'Q') return false;
}
return true;
}
public:
vector<vector<string>> solveNQueens(int n) {
result.clear();
vector<string> chessboard(n, string(n, '.'));
backtracking(n, 0, chessboard);
return result;
}
};
3. C 语言实现
C 语言处理二维字符串数组稍微繁琐一些,需要手动开辟和释放内存,但核心思想完全一致。
objectivec
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
bool isValid(char** chessboard, int row, int col, int n) {
// 检查正上方
for (int i = 0; i < row; i++) {
if (chessboard[i][col] == 'Q') return false;
}
// 检查左上方
for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
if (chessboard[i][j] == 'Q') return false;
}
// 检查右上方
for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
if (chessboard[i][j] == 'Q') return false;
}
return true;
}
void backtracking(int n, int row, char** chessboard, char**** result, int* returnSize) {
if (row == n) {
char** temp = (char**)malloc(n * sizeof(char*));
for (int i = 0; i < n; i++) {
temp[i] = (char*)malloc((n + 1) * sizeof(char));
strcpy(temp[i], chessboard[i]);
}
(*result)[*returnSize] = temp;
(*returnSize)++;
return;
}
for (int col = 0; col < n; col++) {
if (isValid(chessboard, row, col, n)) {
chessboard[row][col] = 'Q';
backtracking(n, row + 1, chessboard, result, returnSize);
chessboard[row][col] = '.'; // 回溯
}
}
}
char*** solveNQueens(int n, int* returnSize, int** returnColumnSizes) {
char*** result = (char***)malloc(1000 * sizeof(char**));
*returnSize = 0;
char** chessboard = (char**)malloc(n * sizeof(char*));
for (int i = 0; i < n; i++) {
chessboard[i] = (char*)malloc((n + 1) * sizeof(char));
for (int j = 0; j < n; j++) chessboard[i][j] = '.';
chessboard[i][n] = '\0';
}
backtracking(n, 0, chessboard, &result, returnSize);
*returnColumnSizes = (int*)malloc(*returnSize * sizeof(int));
for (int i = 0; i < *returnSize; i++) {
(*returnColumnSizes)[i] = n;
}
// 释放初始化的 chessboard
for(int i=0; i<n; i++) free(chessboard[i]);
free(chessboard);
return result;
}
四、 总结与高频避坑指南
在实际敲代码的过程中,逻辑懂了不代表能一遍过(作者本人就踩过坑!)。以下是大家最容易犯的几个错误,请务必避雷:
坑点 1:Python 中的变量赋值越界
在 Python 中,棋盘 qipan 是一个字符串列表(List[str])。
-
错误写法 :
qipan = qipan[row][:col] + "Q" + qipan[row][col+1:]- 后果 :这会把原本包含 N 个字符串的
qipan列表,直接覆盖 成了一个单独的字符串!导致递归进入下一层时,发生严重的IndexError索引越界。
- 后果 :这会把原本包含 N 个字符串的
-
正确写法 :
qipan[row] = qipan[row][:col] + "Q" + qipan[row][col+1:]- 正解 :我们必须精准定位,只修改
qipan列表中的第 row 个元素。
- 正解 :我们必须精准定位,只修改
坑点 2:验证函数忘记返回 True
在写 isValid 时,我们很自然地会把所有的 if (冲突) return False 写完。
-
后果 :如果函数最后忘记写
return True,Python 会默认返回None(被当作False处理),C++ 则会导致未定义行为。这会导致程序认为整个棋盘都没有合法位置,最终输出空集[]。 -
正解 :永远记住在所有冲突检查的
for/while循环结束后,加上一个兜底的return True(意味着历经九九八十一难,终于安全了)。
总结
N皇后问题并没有想象中那么难,只要抓住"按行递归,按列遍历"的核心,并在验证时把控好边界,就能轻松拿下。希望这篇博客对你有所帮助。
照例贴上卡哥的代码随想录
51. N皇后 | 回溯 | N皇后 | 剪枝 | 代码随想录-全网最全算法数据结构刷题学习路线|图文+视频教程|免费开源