【LeetCode刷题日记】51.N皇后

🔥个人主页:代码不加冰(欢迎来访)

🎬作者简介:java后端学习者

❄️个人专栏:LeetCode刷题日记 ,苍穹外卖日记SSM框架深入JavaWeb

命运的结局尽可永在,不屈的挑战却不可须臾或缺!


前言:

大家好我是代码不加冰,好久没有刷题了,刚好暑假开始了,我们继续猛攻算法,同时八股也要带着同步进行,然后就是agent和一些中间件的学习,任务还是比较多的,但还是先从一个地方开始,坚持下来,并没有看着那么的难,让我们一起努力!


摘要

本文探讨了经典回溯算法问题------N皇后问题,要求在N×N棋盘上放置N个互不攻击的皇后(不能同列、同行或同对角线)。作者从题目解析入手,强调回溯法的适用性,并详细拆解两种实现方案:

  1. 二维数组法:使用布尔矩阵记录皇后位置,通过逐行放置、检查列及对角线冲突,回溯撤销无效选择。
  2. 一维数组优化 :以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 < nrow < 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;
    }
}
结语:

这道题虽然很容易看出来整体的写法,但是具体的内在逻辑需要我们认真去弄明白,算是一个难题,大家加油!

相关推荐
古城小栈10 小时前
为啥说:训练用BF16,推理用FP16
人工智能·算法·机器学习
KaMeidebaby10 小时前
卡梅德生物技术快报|蛋白 N 端测序在重组贻贝融合蛋白表征中的应用,解决原核表达序列偏移工艺难题
前端·人工智能·物联网·算法·百度
Turbo正则11 小时前
群论在AI中的应用概述
人工智能·算法·抽象代数
ysa05103011 小时前
【并查集】判环
c++·笔记·算法
Jerry11 小时前
KeetCode 44. 开发商购买土地
算法
Jerry11 小时前
KeetCode 58. 区间和
算法
Jerry12 小时前
LeetCode 209. 长度最小的子数组
算法
彦为君13 小时前
算法思维与经典智力题
java·前端·redis·算法
智能优化与强化学习13 小时前
Gym(Gymnasium)仿真环境详解(二):环境简介、入门算法、调参要点、核心挑战
算法·强化学习·gym·零基础入门·算法评估