

🔥个人主页:代码不加冰(欢迎来访)
🎬作者简介:java后端学习者
❄️个人专栏:LeetCode刷题日记 ,苍穹外卖日记,SSM框架深入,JavaWeb,
✨命运的结局尽可永在,不屈的挑战却不可须臾或缺!
前言:
大家好我是代码不加冰,好久没有刷题了,刚好暑假开始了,我们继续猛攻算法,同时八股也要带着同步进行,然后就是agent和一些中间件的学习,任务还是比较多的,但还是先从一个地方开始,坚持下来,并没有看着那么的难,让我们一起努力!
摘要:
本文探讨了经典回溯算法问题------N皇后问题,要求在N×N棋盘上放置N个互不攻击的皇后(不能同列、同行或同对角线)。作者从题目解析入手,强调回溯法的适用性,并详细拆解两种实现方案:
- 二维数组法:使用布尔矩阵记录皇后位置,通过逐行放置、检查列及对角线冲突,回溯撤销无效选择。
- 一维数组优化 :以
cols[row]=col记录每行皇后列号,通过数学公式快速判断冲突(行±列差/和相等),提升效率。
代码示例展示了两种解法的完整实现,并对比了空间复杂度与可读性。文章强调理解回溯中"做选择-递归-撤销"的核心逻辑,并通过调试技巧帮助掌握算法细节,适合算法学习者深入练习回溯问题。
题目背景:51.N皇后
按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将
n个皇后放置在n×n的棋盘上,并且使皇后彼此之间不能相互攻击。给你一个整数
n,返回所有不同的 n皇后问题 的解决方案。每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中
'Q'和'.'分别代表了皇后和空位。示例 1:
输入:n = 4 输出:[[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]] 解释:如上图所示,4 皇后问题存在两个不同的解法。示例 2:
输入:n = 1 输出:[["Q"]]提示:
1 <= n <= 9
题目解析:
首先我们要明确核心思路:这道题是典型的回溯算法的问题,因为题目中所说的:在 n×n 的棋盘上放置 n 个皇后,使得它们不能互相攻击(即任意两个皇后不在同一行、同一列、同一对角线上)。
| 特征 | N 皇后的对应 |
|---|---|
| 需要尝试所有可能性 | 每行都有 n 列可选,要找到所有放置方案 |
| 有约束条件 | 放皇后时,不能和已有的在同一列/对角线 |
| 可以逐步构建 | 一行一行放,先放第 0 行,再放第 1 行...... |
只要满足这三条,第一时间就该想到回溯。
我们在这里总结一下什么问题会用到回溯算法:
看到所有组合/排列/解 + 约束条件 + 逐步构建 → 立刻想到回溯
常见回溯题类型:
-
排列组合:全排列、子集、组合总和
-
棋盘类:N 皇后、数独
-
路径类:迷宫、单词搜索
-
分割类:分割回文串、IP 地址复原
回归本题,既然已经知道了是回溯算法的题型,那么整体的框架我们就比较熟悉了:
基本逻辑:
逐行放置皇后(因为每行只能放一个)。
尝试在当前行的每一列放置皇后。
如果当前位置合法(不与已放皇后冲突),则递归进入下一行。
如果递归失败(无法放满 n 个皇后),则回退(撤销当前选择),尝试下一列。
当成功放置完 n 行时,记录当前棋盘。
对于第一次接触的同学,我们可以先通过二维数组来过渡理解一下整体的逻辑,后面可以使用更高效简单的一维数组。
二维数组逻辑:
用
boolean[n][n]表示棋盘,true表示放皇后,false表示空位
row 表示:接下来要在第几行放皇后关键点深度解析
为什么只检查上方三个方向
因为我们是从上到下逐行放置:
当前在第
row行第
row+1行及以下还没有放皇后所以只需要检查已经放过的行(0 到 row-1)
text
当前行 ↑ 已放行:0, 1, 2 ← 这些行可能有皇后,需要检查 当前行:3 ← 正在放 未放行:4, 5 ... ← 这些行还没放,不用检查为什么不用检查同一行
因为我们每行只放一个皇后:
在
backtrack中,row是固定的放完一个就进入下一行
不会在同一行放第二个
回溯时为什么
board[row][col] = false这是回溯的核心操作:
text
尝试放皇后 → board[row][col] = true 递归下一行 → backtrack(row+1) 如果递归失败 → 撤销选择 → board[row][col] = false 尝试下一列 → col++如果不撤销,棋盘上会留下错误的皇后,影响后续尝试。
二维数组的调试技巧
在 backtrack 中加入打印,观察过程: java private void backtrack(...) { if (row == n) { result.add(generateBoard(board, n)); return; } for (int col = 0; col < n; col++) { if (isValid(board, row, col, n)) { board[row][col] = true; // 打印当前棋盘(调试用) System.out.println("在第 " + row + " 行第 " + col + " 列放皇后"); printBoard(board, n); backtrack(result, board, n, row + 1); // 打印回溯信息(调试用) System.out.println("回溯:撤销 (" + row + "," + col + ")"); board[row][col] = false; } } }总结:
部分 作用 backtrack核心递归,尝试所有可能 isValid剪枝,提前排除不合法位置 board[row][col] = true/false做选择/撤销选择(回溯的灵魂) row == n递归终止条件,找到一个解 三个方向检查 保证皇后不互相攻击
递归的四个参数解析:
参数1:List<List<String>> result
含义:存储所有解的"答案容器"
作用:
-
当找到一个完整的解时,把它添加到这个列表中
-
最终返回给调用者
类型解释:
java
List<List<String>> result
// 外层 List:存储多个解
// 内层 List<String>:存储一个解的棋盘(每个字符串是一行)
例子(n=4 的一个解):
java
result = [
[".Q..", "...Q", "Q...", "..Q."], // 解1
["..Q.", "Q...", "...Q", ".Q.."] // 解2
]
在代码中的使用:
java
if (row == n) {
result.add(generateBoard(board, n)); // 添加一个解
return;
}
参数2:boolean[][] board
含义:当前的棋盘状态
作用:
-
记录哪些位置已经放了皇后
-
board[i][j] = true:第 i 行第 j 列有皇后 -
board[i][j] = false:该位置为空
可视化(n=4):
java
board = [
[false, true, false, false], // 第0行:.Q..
[false, false, false, true ], // 第1行:...Q
[true, false, false, false], // 第2行:Q...
[false, false, true, false] // 第3行:..Q.
]
在代码中的使用:
java
// 放置皇后
board[row][col] = true;
// 检查合法性
if (board[i][col]) return false;
// 回溯撤销
board[row][col] = false;
为什么用 boolean 而不是 char
-
boolean 更节省内存(1字节 vs 2字节)
-
逻辑清晰:
true=有皇后,false=空 -
方便回溯时快速设置
参数3:int n
含义:棋盘的大小(n×n)
作用:
-
确定棋盘的行数和列数
-
判断边界条件(
col < n,row < n) -
生成棋盘字符串时使用
为什么需要传递 n
-
虽然
board.length也能得到 n -
但显式传递更清晰,避免多次调用
board.length -
是递归中的"常量参数",每次调用值不变
在代码中的使用:
java
// 遍历所有列
for (int col = 0; col < n; col++) {
// ...
}
// 检查右边界
if (j < n) { ... }
// 生成棋盘
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
// ...
}
}
参数4:int row
含义:当前要处理的行号(从 0 开始)
作用:
-
表示递归进行到哪一行了
-
决定接下来在哪个位置放皇后
-
是递归"推进"的关键变量
值的含义:
java
row = 0 → 准备放第0行(还没放任何皇后)
row = 1 → 已经放了第0行,准备放第1行
row = 2 → 已经放了第0,1行,准备放第2行
row = n → 已经放了所有行(找到解)
在代码中的使用:
java
// 终止条件
if (row == n) {
// 已经放完所有行
}
// 在当前行尝试每一列
for (int col = 0; col < n; col++) {
if (isValid(board, row, col, n)) {
board[row][col] = true; // 在当前行放皇后
backtrack(result, board, n, row + 1); // 处理下一行
board[row][col] = false;
}
}
| 参数 | 类型 | 变化/不变 | 作用分类 |
|---|---|---|---|
result |
List<List<String>> | 不变(同一个容器) | 答案收集器 |
board |
boolean\[\]\[\] | 变化(不断修改) | 当前状态 |
n |
int | 不变(常量) | 问题规模 |
row |
int | 变化(每次+1) | 递归进度 |
结束之后,我们根据题目要求的,把结果进行转换一下即可。
一维数组的逻辑:
关键点:
用一维数组
cols[row] = col记录每行皇后所在的列,便于回溯。冲突判断:
同一列:
cols[i] == col主对角线(左上到右下):
row - col == i - cols[i]副对角线(右上到左下):
row + col == i + cols[i]解释:
假设 n=4 的棋盘,每个格子用
(行, 列)表示:
text 列0 列1 列2 列3 行0 (0,0)(0,1)(0,2)(0,3) 行1 (1,0)(1,1)(1,2)(1,3) 行2 (2,0)(2,1)(2,2)(2,3) 行3 (3,0)(3,1)(3,2)(3,3)一维数组
cols怎么记录皇后我们逐行放皇后,每行只放一个,所以用一维数组记录:
java cols[row] = col // 第 row 行的皇后放在第 col 列举例:如果棋盘是这样的:
text 行0: . Q . . → 第0行皇后在第1列 行1: . . . Q → 第1行皇后在第3列 行2: Q . . . → 第2行皇后在第0列 行3: . . Q . → 第3行皇后在第2列用
cols数组记录:
java cols[0] = 1 cols[1] = 3 cols[2] = 0 cols[3] = 2所以
cols[i]就是第 i 行的皇后在第几列,这个一定要先搞清楚三、同一列的判断(最简单)
现在我们要在第
row行第col列放新皇后,需要检查之前所有的行i(0 到 row-1)。如果之前某行的皇后也在第
col列,就冲突了
java if (cols[i] == col) { // 冲突!第 i 行和第 row 行在同一列 }图示:
text 列0 列1 列2 列3 行0 . Q . . ← cols[0]=1 行1 . . . Q ← cols[1]=3 行2 . ? . . ← 想在 (2,1) 放 检查:cols[0]=1,col=1 → 相等!冲突!四、主对角线判断
第1步:先看规律
主对角线是 左上到右下(\ 方向)。
在这条线上的格子,行号 - 列号 的值都相等
看棋盘上的例子:
text 主对角线1: (0,0): 0-0 = 0 (1,1): 1-1 = 0 ← 相同! (2,2): 2-2 = 0 ← 相同! (3,3): 3-3 = 0 ← 相同! 主对角线2: (0,1): 0-1 = -1 (1,2): 1-2 = -1 ← 相同! (2,3): 2-3 = -1 ← 相同! 主对角线3: (1,0): 1-0 = 1 (2,1): 2-1 = 1 ← 相同! (3,2): 3-2 = 1 ← 相同!结论 :如果两个格子在同一主对角线 → 行号-列号 相等
第2步:应用到 N 皇后
已有皇后 :在第
i行,列是cols[i]→ 它的
行号-列号 = i - cols[i]新皇后 :在第
row行,列是col→ 它的
行号-列号 = row - col如果它们在同一主对角线:
text i - cols[i] == row - col写成代码:
java if (row - col == i - cols[i]) { // 冲突!在同一主对角线 }五、副对角线判断(同理)
第1步:规律
副对角线是 右上到左下(/ 方向)。
在这条线上的格子,行号 + 列号 的值都相等!
text 副对角线1: (0,3): 0+3 = 3 (1,2): 1+2 = 3 ← 相同! (2,1): 2+1 = 3 ← 相同! (3,0): 3+0 = 3 ← 相同! 副对角线2: (0,2): 0+2 = 2 (1,1): 1+1 = 2 ← 相同! (2,0): 2+0 = 2 ← 相同!结论 :如果两个格子在同一副对角线 → 行号+列号 相等
第2步:应用到 N 皇后
已有皇后 :在第
i行,列是cols[i]→ 它的
行号+列号 = i + cols[i]新皇后 :在第
row行,列是col→ 它的
行号+列号 = row + col如果它们在同一副对角线:
text i + cols[i] == row + col写成代码:
java if (row + col == i + cols[i]) { // 冲突!在同一副对角线 } text 棋盘上的坐标: 列0 列1 列2 列3 行0 (0,0)(0,1)(0,2)(0,3) 行1 (1,0)(1,1)(1,2)(1,3) 行2 (2,0)(2,1)(2,2)(2,3) 行3 (3,0)(3,1)(3,2)(3,3)判断冲突的三个公式:
1. 同一列: colsi == col (列号相同)
2. 主对角线: i - colsi == row - col (行减列的差相同) \ 方向
3. 副对角线: i + colsi == row + col (行加列的和相同) / 方向
一维 vs 二维对比
| 对比维度 | 一维数组 cols[] |
二维数组 board[][] |
|---|---|---|
| 空间复杂度 | O(n) | O(n²) |
| 冲突检查 | 数学计算(快) | 循环遍历(慢) |
| 代码可读性 | 需要理解数学规律 | 直观,容易理解 |
| 回溯操作 | cols[row] = col |
board[row][col] = true/false |
| 生成结果 | 需要构造棋盘 | 直接使用棋盘 |
题目答案:
二维数组解法:
import java.util.*;
public class Solution {
public List<List<String>> solveNQueens(int n) {
List<List<String>> result = new ArrayList<>();
// 二维棋盘:true 表示有皇后,false 表示空
boolean[][] board = new boolean[n][n];
backtrack(result, board, n, 0);
return result;
}
private void backtrack(List<List<String>> result, boolean[][] board, int n, int row) {
// 找到一组解
if (row == n) {
result.add(generateBoard(board, n));
return;
}
// 尝试当前行的每一列
for (int col = 0; col < n; col++) {
if (isValid(board, row, col, n)) {
board[row][col] = true; // 放置皇后
backtrack(result, board, n, row + 1);
board[row][col] = false; // 回溯撤销
}
}
}
// 检查在 (row, col) 放皇后是否合法
private boolean isValid(boolean[][] board, int row, int col, int n) {
// 1. 检查同一列(上方)
for (int i = 0; i < row; i++) {
if (board[i][col]) return false;
}
// 2. 检查左上对角线
for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
if (board[i][j]) return false;
}
// 3. 检查右上对角线
for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
if (board[i][j]) return false;
}
return true;
}
// 将 boolean 数组转为题目要求的 List<String>
private List<String> generateBoard(boolean[][] board, int n) {
List<String> result = new ArrayList<>();
for (int i = 0; i < n; i++) {
StringBuilder sb = new StringBuilder();
for (int j = 0; j < n; j++) {
sb.append(board[i][j] ? 'Q' : '.');
}
result.add(sb.toString());
}
return result;
}
}
一维数组解法:
import java.util.*;
public class Solution {
public List<List<String>> solveNQueens(int n) {
List<List<String>> result = new ArrayList<>();
// 用数组记录每行皇后所在的列,初始值为 -1 表示未放置
int[] cols = new int[n];
Arrays.fill(cols, -1);
backtrack(result, cols, n, 0);
return result;
}
// 回溯:当前处理 row 行
private void backtrack(List<List<String>> result, int[] cols, int n, int row) {
// 如果已经放完所有行,说明找到一个有效解
if (row == n) {
result.add(generateBoard(cols, n));
return;
}
// 尝试当前行的每一列
for (int col = 0; col < n; col++) {
if (isValid(cols, row, col)) {
cols[row] = col; // 放置皇后
backtrack(result, cols, n, row + 1); // 递归下一行
cols[row] = -1; // 撤销(回溯)
}
}
}
// 检查在 (row, col) 放置皇后是否合法
private boolean isValid(int[] cols, int row, int col) {
for (int i = 0; i < row; i++) {
// 同一列
if (cols[i] == col) {
return false;
}
// 主对角线(差相等)
if (row - col == i - cols[i]) {
return false;
}
// 副对角线(和相等)
if (row + col == i + cols[i]) {
return false;
}
}
return true;
}
// 将 cols 数组转换为题目要求的 List<String> 棋盘
private List<String> generateBoard(int[] cols, int n) {
List<String> board = new ArrayList<>();
for (int i = 0; i < n; i++) {
char[] row = new char[n];
Arrays.fill(row, '.');
row[cols[i]] = 'Q';
board.add(new String(row));
}
return board;
}
}
结语:
这道题虽然很容易看出来整体的写法,但是具体的内在逻辑需要我们认真去弄明白,算是一个难题,大家加油!
