力扣 51. N 皇后:基础回溯、布尔数组优化、位运算全解(Java 实现)

一、引言

LeetCode 51 N 皇后问题是回溯算法 的绝对经典标杆题,更是面试中考察递归逻辑、剪枝思维与空间优化的核心考点。这道题的核心是破解 "如何在 N×N 棋盘上摆放 N 个皇后,使其互不攻击 ",而基础回溯遍历校验、布尔数组优化、位运算优化则是解决该问题的三大进阶思路 ------ 三者从暴力到优雅、从直观到极致,在时间 / 空间复杂度与代码抽象程度上层层递进,完美对应了算法思维从入门到高阶的成长路径。

本文会系统拆解这三种解法:先通过基础回溯法 理清最直观的 "逐行枚举 + 循环校验" 逻辑(易理解但效率偏低);再升级为布尔数组优化版 ,用 O (1) 时间完成冲突校验(可读性与性能的最佳平衡);最后揭秘位运算优化版,通过二进制状态压缩实现极致性能(面试高阶考点!),并全部给出可直接运行的 Java 代码,帮你从原理到实现彻底啃下 N 皇后。

二、N皇后规则

皇后问题里,皇后的攻击范围非常大:

  • 同一不能有两个皇后
  • 同一不能有两个皇后
  • 两条对角线(左上↔右下、右上↔左下)上也不能有两个皇后

只要满足这三条,就是一个合法放置方案。

三、回溯思想

你可以把回溯理解成:

一条路走到黑,走不通就回头,换一条路再试。

放到 N 皇后里就是:

  1. 选择:在当前行选一列,试着放一个皇后
  2. 判断:检查这一列、两条对角线是否冲突
  3. 递归:如果不冲突,就去下一行继续放皇后
  4. 回溯
    • 要么放完所有行 → 找到一组解
    • 要么走到某行没位置可放 → 撤销当前皇后,回到上一行,换一列再试

3.1 遍历校验

java 复制代码
// 判断在 (row, col) 放皇后是否合法
private boolean isValid(char[][] board, int row, int col) {
    int n = board.length;

    // 1. 检查当前列(上方所有行)
    for (int i = 0; i < row; i++) {
        if (board[i][col] == 'Q') {
            return false;
        }
    }

    // 2. 检查左上对角线 ↖
    for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
        if (board[i][j] == 'Q') {
            return false;
        }
    }

    // 3. 检查右上对角线 ↗
    for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
        if (board[i][j] == 'Q') {
            return false;
        }
    }

    return true;
}

行就不校验了,默认一行放一个。

3.2 布尔校验

我们可以用布尔数组替代集合 / 逐行遍历的优化版本,核心是用数组下标直接映射「列 / 对角线」的占用状态,实现 O (1) 校验。

java 复制代码
// 列方向:下标 = 列号col,长度n(0~n-1共n列)
usedCol = new boolean[n];
// 45°对角线(左上→右下,row-col为定值):下标 = row - col + n - 1(偏移量避免负数),长度2n-1
usedDiag45 = new boolean[2 * n - 1];
// 135°对角线(右上→左下,row+col为定值):下标 = row + col,长度2n-1
usedDiag135 = new boolean[2 * n - 1];

|-------|------|-----|-----|
| 0 , 0 | 0 ,1 | 0,2 | 0,3 |
| 1 ,0 | 1,1 | 1,2 | 1,3 |
| 2 ,0 | 2,1 | 2,2 | 2,3 |
| 3 ,0 | 3,1 | 3,2 | 3,3 |

我们来具体看一下对角线

|-------|-------|-------|-------|
| \(3) | \(2) | \(1) | \(0) |
| \(4) | \(3) | \(2) | \(1) |
| \(5) | \(4) | \(3) | \(2) |
| \(6) | \(5) | \(4) | \(3) |

斜对角线个数(00-33)1 + (01-23,10-32) 2 + (02-13 , 20-31)2+(03,30)2 也就是2(n - 1)+1

我们再来找一下规律

由于是同一个对角线,那么一定row - col是相等的

并且对角线是沿着中间那条对称的,那我们可以干脆把中间那条的索引定位n - 1

以此类推,其他索引就是row - col + n - 1

左上-右下对角线说完了,我们再来看右上-左下对角线,其实很明显 ,同一条对角线是row + col 相等

我们再从数学的角度来理解

左上-右下对角线是y = x + b ,那么同一条对角线 y - x = b 相等(row -col or col - row)

右上-左下对角线是 y = -x + b,那么同一条对角线 y + x = b相等(row + col)

那么我们的判断条件就可以简化成下面这样了

java 复制代码
!(usedCol[col] | usedDiag45[row + col] | usedDiag135[row - col + n - 1])

四、基础回溯

这里我们采用遍历校验,方便大家来理解

4.1 回溯思想

java 复制代码
    /**
     * 回溯函数:逐行放置皇后
     * @param board 当前棋盘状态
     * @param row 当前处理的行号
     */
    private void backtrack(char[][] board, int row) {
        int n = board.length;
        // 递归终止条件:所有行都放置完成
        if (row == n) {
            result.add(charToList(board));
            return;
        }

        // 遍历当前行的每一列,尝试放置皇后
        for (int col = 0; col < n; col++) {
            // 校验当前位置是否合法
            if (isValid(board, row, col)) {
                // 放置皇后
                board[row][col] = 'Q';
                // 递归处理下一行
                backtrack(board, row + 1);
                // 回溯:撤销皇后
                board[row][col] = '.';
            }
        }
    }

这里我们来理解一下这里的回溯思想,看看是如何一步步成功求解的。

失败情况

放置 -> 递归进入下一行->判断能不能放-> 不能就继续判断下一列->全部都不行(递归结束,结果没有被添加到res中)-> 回溯到放置皇后的地方,撤销放置皇后 - > 重新开始放置下一个皇后

成功情况

放置 -> 递归进入下一行->判断能不能放-> 不能就继续判断下一列->寻找到可行的->继续递归直到达到最后一行放置完毕-> 将当前解放到res中存储-> 当前解应该被释放,回溯,撤销放置n皇后-> 开始重新放置下一个n皇后

4.2 完整代码

java 复制代码
import java.util.*;

class Solution {
    List<List<String>> result = new ArrayList<>();

    public List<List<String>> solveNQueens(int n) {
        // 初始化棋盘,全部为 '.'
        char[][] board = new char[n][n];
        for (char[] row : board) {
            Arrays.fill(row, '.');
        }
        backtrack(board, 0);
        return result;
    }

    /**
     * 回溯函数:逐行放置皇后
     * @param board 当前棋盘状态
     * @param row 当前处理的行号
     */
    private void backtrack(char[][] board, int row) {
        int n = board.length;
        // 递归终止条件:所有行都放置完成
        if (row == n) {
            result.add(charToList(board));
            return;
        }

        // 遍历当前行的每一列,尝试放置皇后
        for (int col = 0; col < n; col++) {
            // 校验当前位置是否合法
            if (isValid(board, row, col)) {
                // 放置皇后
                board[row][col] = 'Q';
                // 递归处理下一行
                backtrack(board, row + 1);
                // 回溯:撤销皇后
                board[row][col] = '.';
            }
        }
    }

    /**
     * 校验 (row, col) 位置是否可以放置皇后
     */
    private boolean isValid(char[][] board, int row, int col) {
        int n = board.length;
        // 1. 校验列方向(上方)
        for (int i = 0; i < row; i++) {
            if (board[i][col] == 'Q') {
                return false;
            }
        }

        // 2. 校验左上对角线
        for (int i = row - 1, j = col - 1; i >= 0 && j >= 0; i--, j--) {
            if (board[i][j] == 'Q') {
                return false;
            }
        }

        // 3. 校验右上对角线
        for (int i = row - 1, j = col + 1; i >= 0 && j < n; i--, j++) {
            if (board[i][j] == 'Q') {
                return false;
            }
        }

        return true;
    }

    /**
     * 将 char[][] 棋盘转换为 List<String>
     */
    private List<String> charToList(char[][] board) {
        List<String> list = new ArrayList<>();
        for (char[] row : board) {
            list.add(new String(row));
        }
        return list;
    }
}

4.3 复杂度分析

  • 时间复杂度 :O(n!),第一行有 n 种选择,第二行最多 n-1 种,第三行最多 n-2 种,以此类推,总共有 n×(n−1)×(n−2)×...×1=n! 种可能。
  • 空间复杂度:O(n2),棋盘占用 n2 空间,递归栈深度为 n,总空间为 O(n2)。

五、布尔数组优化

这里呢,我们就采用布尔数组来O(1)校验

5.1 回溯思想

java 复制代码
    private void backtrack(char[][] board, int row) {
        int n = board.length;
        if (row == n) {
            res.add(construct(board));
            return;
        }

        for (int col = 0; col < n; col++) {
            int d45 = row - col + n - 1;
            int d135 = row + col;

            // O(1) 冲突判断
            if (usedCol[col] || usedDiag45[d45] || usedDiag135[d135]) continue;

            // 标记占用
            usedCol[col] = true;
            usedDiag45[d45] = true;
            usedDiag135[d135] = true;
            board[row][col] = 'Q';

            backtrack(board, row + 1);

            // 回溯撤销
            board[row][col] = '.';
            usedCol[col] = false;
            usedDiag45[d45] = false;
            usedDiag135[d135] = false;
        }
    }

失败情况

放置(同时标记数组) -> 递归进入下一行->判断能不能放-> 不能就继续判断下一列->全部都不行(递归结束,结果没有被添加到res中)-> 回溯到放置皇后的地方,撤销放置皇后(将标记数组同步回溯) - > 重新开始放置下一个皇后

成功情况

放置(同时标记数组) -> 递归进入下一行->判断能不能放-> 不能就继续判断下一列->寻找到可行的->继续递归直到达到最后一行放置完毕-> 将当前解放到res中存储-> 当前解应该被释放,回溯,撤销放置n皇后(还有标记数组)-> 开始重新放置下一个n皇后

5.2 完整代码

java 复制代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

// 解法2:回溯 + 布尔数组标记(O(1)校验)
public class NQueensSolution2 {
    List<List<String>> res = new ArrayList<>();
    boolean[] usedCol;      // 标记列是否被占用
    boolean[] usedDiag45;   // 标记 左上->右下 对角线
    boolean[] usedDiag135;  // 标记 右上->左下 对角线

    public List<List<String>> solveNQueens(int n) {
        usedCol = new boolean[n];
        usedDiag45 = new boolean[2 * n - 1];
        usedDiag135 = new boolean[2 * n - 1];

        char[][] board = new char[n][n];
        for (int i = 0; i < n; i++) Arrays.fill(board[i], '.');

        backtrack(board, 0);
        return res;
    }

    private void backtrack(char[][] board, int row) {
        int n = board.length;
        if (row == n) {
            res.add(construct(board));
            return;
        }

        for (int col = 0; col < n; col++) {
            int d45 = row - col + n - 1;
            int d135 = row + col;

            // O(1) 冲突判断
            if (usedCol[col] || usedDiag45[d45] || usedDiag135[d135]) continue;

            // 标记占用
            usedCol[col] = true;
            usedDiag45[d45] = true;
            usedDiag135[d135] = true;
            board[row][col] = 'Q';

            backtrack(board, row + 1);

            // 回溯撤销
            board[row][col] = '.';
            usedCol[col] = false;
            usedDiag45[d45] = false;
            usedDiag135[d135] = false;
        }
    }

    private List<String> construct(char[][] board) {
        List<String> path = new ArrayList<>();
        for (char[] row : board) path.add(new String(row));
        return path;
    }
}

六、位运算

6.1 状态变量

我们不再用数组 / 集合,而是用一个 int 整型的二进制位来记录占用状态:

  • 每一位二进制:1 = 已占用0 = 可放皇后
  • 用 3 个 int 变量记录所有冲突:
    1. col:列占用状态
    2. diag1:左上→右下对角线状态
    3. diag2:右上→左下对角线状态
  • :一共 n
  • 对角线 :一共 2n-1

位运算根本不存储 "第几条对角线" ,而是存储:这条对角线会影响下一行的哪一列

col、diag1、diag2 三个变量 每一位的位置,都对应同一列

n=4,棋盘是 4 列:

  • 二进制 第 0 位 → 第 0 列
  • 二进制 第 1 位 → 第 1 列
  • 二进制 第 2 位 → 第 2 列
  • 二进制 第 3 位 → 第 3 列

不管是列、还是对角线,全部都用这 4 个二进制位表示

我们设p为要放置的第n列 , p =0010(放置第一列)

那么col = col | p ,假设col = 0001(只放置了第0列),现在col | p就是0011(放置了第0列和第1列)

那么左上-右下对角线diag1 = (diag1 | p) << 1与p或计算再右移,我们来想想假设我们放在(1,1),那么你在第二行放的时候是不是(2,2)不能放了,也就是第2列不能放了,从第一列不能放到第二列不能放,是不是要右移一位。

那么左下-右上对角线也是同理,我们再(1,1)放了,在第二行(0,2)也不能放了,从第一列不能放到成为第0列不能放,是不是要左移一位,那么就是**(diag2 | p) >> 1**

6.2 位校验

那么我们现在搞清楚了这些状态变量,来看看怎么判断能不能放吧。

首先是col | diag1 | diag2列、两条对角线的列占用位置合并

~(...) 按位取反,这样我们得到的就是可以用的列

但是我们知道int的位数肯定是大于等于n的,我们直接取反,高位就会全部变成1,多出了很多无效的n,所以我们应该只取最低的n位,前面的高位都应该是0

(1 << n) - 1 生成掩码

  • 例 n=4 → 1<<4 = 10000 → 减 1 → 01111
  • 作用:只保留低 n 位,高位全部清 0

& 掩码

  • 清理高位垃圾
  • 最终得到:当前行所有合法可放位置

总结:

java 复制代码
int available = (~(col | diag1 | diag2)) & ((1 << n) - 1);

6.3 遍历

这次我们得到的available不是一个boolean了,而是一个int,上面记录所有可以放置的n,那么按照从小到大遍历的顺序,我们应该找出col最小的那位

java 复制代码
int p = available & -available;

这是位运算经典技巧:提取最右边的 1

在二进制里:一个数 取负数(-x) = 全部取反 + 1

从右边第一个 1 开始,左边全部翻转,右边全部不变

然后一 &就只剩下最右边的 1

6.4 回溯

接下来我们来思考一下位计算是怎么回溯的

我们已经知道available中存放的是用过的列

java 复制代码
available &= available - 1;

例:available = 01010→ 减 1 → 01001→ & 后 → 01000

作用:

试过这一列了,把它从可选中去掉,继续试下一列。

6.5 回溯思想

java 复制代码
/**
     * 位运算回溯
     * @param row  当前处理行
     * @param col  列占用状态
     * @param diag1 左上->右下对角线状态
     * @param diag2 右上->左下对角线状态
     */
    private void backtrack(char[][] board, int row, int col, int diag1, int diag2) {
        int n = board.length;
        // 递归终止:放完所有行
        if (row == n) {
            res.add(construct(board));
            return;
        }

        // 1. 计算当前行所有可放皇后的位置(1=可放)
        int available = (~(col | diag1 | diag2)) & ((1 << n) - 1);

        // 2. 遍历所有可放位置(直到 available=0)
        while (available != 0) {
            // 3. 取出最低位 1(选一个位置放皇后)
            int p = available & -available;
            // 4. 计算当前列号
            int c = Integer.bitCount(p - 1);

            // 放皇后
            board[row][c] = 'Q';

            // 5. 递归下一行:更新三个状态
            backtrack(board,
                    row + 1,
                    col | p,                // 新列状态
                    (diag1 | p) << 1,       // 新对角线1(左移)
                    (diag2 | p) >> 1        // 新对角线2(右移)
            );

            // 回溯撤销皇后
            board[row][c] = '.';

            // 6. 去掉已处理的最低位 1
            available &= available - 1;
        }
    }

失败情况

放置(标记当前列,左上到右下对角线右移一位,左下到右上对角线左移一位) -> 递归进入下一行->判断能不能放(对列,左右对角线进行判断,并取有效低n位,获得所有可放列)-> 全部都不行(递归结束,结果没有被添加到res中)-> 回溯到放置皇后的地方,撤销放置皇后(将available中的最低1改成0,col这些不用改,因为传过去的时候都是临时变量) - > 重新开始放置下一个皇后

成功情况

放置(标记当前列,左上到右下对角线右移一位,左下到右上对角线左移一位) -> 递归进入下一行->判断能不能放(对列,左右对角线进行判断,并取有效低n位,获得所有可放列)-> 寻找到可行的->继续递归直到达到最后一行放置完毕-> 将当前解放到res中存储-> 当前解应该被释放,回溯,撤销放置n皇后(将available中的最低1改成0,col这些不用改,因为传过去的时候都是临时变量)-> 开始重新放置下一个n皇后

6.6 完整代码

java 复制代码
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

// 解法3:位运算优化回溯(最高效、面试高阶版)
public class NQueensSolution3 {
    List<List<String>> res = new ArrayList<>();

    public List<List<String>> solveNQueens(int n) {
        char[][] board = new char[n][n];
        for (int i = 0; i < n; i++) Arrays.fill(board[i], '.');
        // 初始状态:col=0, diag1=0, diag2=0 都没占用
        backtrack(board, 0, 0, 0, 0);
        return res;
    }

    /**
     * 位运算回溯
     * @param row  当前处理行
     * @param col  列占用状态
     * @param diag1 左上->右下对角线状态
     * @param diag2 右上->左下对角线状态
     */
    private void backtrack(char[][] board, int row, int col, int diag1, int diag2) {
        int n = board.length;
        // 递归终止:放完所有行
        if (row == n) {
            res.add(construct(board));
            return;
        }

        // 1. 计算当前行所有可放皇后的位置(1=可放)
        int available = (~(col | diag1 | diag2)) & ((1 << n) - 1);

        // 2. 遍历所有可放位置(直到 available=0)
        while (available != 0) {
            // 3. 取出最低位 1(选一个位置放皇后)
            int p = available & -available;
            // 4. 计算当前列号
            int c = Integer.bitCount(p - 1);

            // 放皇后
            board[row][c] = 'Q';

            // 5. 递归下一行:更新三个状态
            backtrack(board,
                    row + 1,
                    col | p,                // 新列状态
                    (diag1 | p) << 1,       // 新对角线1(左移)
                    (diag2 | p) >> 1        // 新对角线2(右移)
            );

            // 回溯撤销皇后
            board[row][c] = '.';

            // 6. 去掉已处理的最低位 1
            available &= available - 1;
        }
    }

    private List<String> construct(char[][] board) {
        List<String> path = new ArrayList<>();
        for (char[] row : board) path.add(new String(row));
        return path;
    }
}

七、总结

方案 校验速度 空间 难度 适用场景
基础遍历校验 O(n) 入门 理解回溯
布尔数组优化 O(1) 简单 工程 / 面试首选
位运算优化 O (1) 指令级 极低 高阶 最优解、冲排名
相关推荐
漫霂2 小时前
二叉树的翻转
java·数据结构·算法
熊猫钓鱼>_>2 小时前
从零构建大模型可调用的Skill:基于Function Calling的完整指南
人工智能·算法·语言模型·架构·agent·skill·functioncall
py有趣2 小时前
力扣热门100题之螺旋矩阵
算法·leetcode
程序猿阿越2 小时前
Kafka4源码(三)Share Group共享组
java·后端·源码阅读
亦暖筑序2 小时前
让AI不再"一本正经胡说八道":Spring AI RAG与VectorStore源码全解
java·源码阅读
xiaoyaohou112 小时前
003、轻量化改进(一):网络剪枝原理与实战
算法·机器学习·剪枝
蒙奇·D·路飞-2 小时前
大模型时代下 Java 后端开发的技术重构与工程实践
java·开发语言·重构
我是章汕呐2 小时前
政策评估的“黄金标准”:DID模型从原理到Stata实操
大数据·人工智能·经验分享·算法·回归
2301_822703203 小时前
光影进度条:鸿蒙Flutter实现动态光影效果的进度条
算法·flutter·华为·信息可视化·开源·harmonyos