LeetCode 51 - N皇后问题 详解笔记

1. 问题概述

1.1 题目描述

n皇后问题 研究的是如何将 n 个皇后放置在 n × n 的棋盘上,并且使皇后彼此之间不能相互攻击。

输入 :整数 n
输出 :所有不同的n皇后问题的解决方案
表示方法'Q' 表示皇后,'.' 表示空位

1.2 攻击规则

皇后可以攻击同一行、同一列、同一斜线上的任何棋子。因此n皇后问题要求:

  • 任意两个皇后不能处于同一行
  • 任意两个皇后不能处于同一列
  • 任意两个皇后不能处于同一斜线(包括左斜线和右斜线)

1.3 示例

复制代码
示例 1:
输入: n = 4
输出: [[".Q..","...Q","Q...","..Q."],["..Q.","Q...","...Q",".Q.."]]
解释: 4皇后问题存在两个不同的解法

解法1:          解法2:
 . Q . .        . . Q .
 . . . Q        Q . . .
 Q . . .        . . . Q
 . . Q .        . Q . .

示例 2:
输入: n = 1
输出: [["Q"]]

1.4 约束条件

  • 1 ≤ n ≤ 9

2. 解题思路 - 回溯算法

2.1 核心思想

使用回溯算法逐行放置皇后:

  1. 从第0行开始,依次在每一行放置一个皇后
  2. 对于当前行,尝试在每一列放置皇后
  3. 检查当前位置是否与已放置的皇后冲突
  4. 如果不冲突,放置皇后并递归处理下一行
  5. 递归返回后,撤销当前皇后的放置(回溯)
  6. 当所有行都放置完毕,找到一个解

2.2 为什么逐行放置?

  • 自动避免行冲突:每行只放一个皇后,确保不会有两个皇后在同一行
  • 降低复杂度:将二维搜索转化为一维搜索,只需考虑列和斜线冲突

3. 冲突检测策略(核心)

3.1 冲突检测数组

使用三个布尔数组实现O(1)时间的冲突检测:

数组 含义 索引计算 大小 说明
ca[n] 列冲突 ca[j] n ca[j]=true 表示第j列已有皇后
cb[2n-1] 左斜线冲突 cb[i+j] 2n-1 cb[i+j]=true 表示该左斜线已有皇后
cc[2n-1] 右斜线冲突 cc[n-1-(i-j)] 2n-1 cc[n-1-(i-j)]=true 表示该右斜线已有皇后

3.2 斜线索引计算详解

3.2.1 左斜线(↘方向)

特点 :同一左斜线上的格子,i + j 值为常数

索引计算index = i + j
范围[0, 2n-2](共 2n-1 条左斜线)

n=4时的左斜线索引标注

复制代码
    j=0  j=1  j=2  j=3
i=0   0    1    2    3
i=1   1    2    3    4
i=2   2    3    4    5
i=3   3    4    5    6
3.2.2 右斜线(↙方向)

特点 :同一右斜线上的格子,i - j 值为常数

索引计算index = n - 1 - (i - j)
目的 :将 i - j 的范围 [-(n-1), n-1] 映射到 [0, 2n-2]
范围[0, 2n-2](共 2n-1 条右斜线)

n=4时的右斜线索引标注

复制代码
    j=0  j=1  j=2  j=3
i=0   3    4    5    6
i=1   2    3    4    5
i=2   1    2    3    4
i=3   0    1    2    3

4. 算法实现详解

4.1 整体流程

java 复制代码
主函数 solveNQueens():
1. 初始化冲突检测数组 ca, cb, cc
2. 初始化棋盘 table(全部填充'.')
3. 创建结果集 result
4. 调用 dfs(0, ...) 从第0行开始搜索
5. 返回 result

回溯函数 dfs(i, ...):
1. 如果 i == n(所有行处理完)
   - 将当前棋盘转换为字符串列表
   - 加入result,返回
2. 遍历当前行i的每一列j
   - 检查冲突:ca[j] || cb[i+j] || cc[n-1-(i-j)]
   - 如果冲突,跳过
   - 放置皇后,标记冲突
   - 递归处理下一行:dfs(i+1, ...)
   - 回溯:撤销皇后,清除冲突标记

4.2 关键代码实现

java 复制代码
package com.it.X_回溯;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public class NQueenLeetcode51 {

    public static void main(String[] args) {
        System.out.println(solveNQueens(4)); // 4皇后问题
        System.out.println(solveNQueens(1)); // 1皇后问题
    }

    /**
     * N皇后问题求解主函数
     * @param n 棋盘大小
     * @return 所有可行的解决方案
     */
    static List<List<String>> solveNQueens(int n) {
        // 1. 初始化列冲突检测数组
        boolean[] ca = new boolean[n];
        
        // 2. 初始化左斜线冲突检测数组(↘方向)
        // 大小为 2n-1,因为 i+j 的范围是 [0, 2n-2]
        boolean[] cb = new boolean[2 * n - 1];
        
        // 3. 初始化右斜线冲突检测数组(↙方向)
        // 大小为 2n-1,因为 i-j 的范围是 [-(n-1), n-1]
        boolean[] cc = new boolean[2 * n - 1];
        
        // 4. 初始化棋盘,全部填充为 '.'(空位)
        char[][] table = new char[n][n];
        for (char[] chars : table) {
            Arrays.fill(chars, '.');
        }

        // 5. 存储所有解决方案
        List<List<String>> result = new ArrayList<>();
        
        // 6. 从第0行开始回溯搜索
        dfs(0, n, table, ca, cb, cc, result);
        
        return result;
    }

    /**
     * 深度优先搜索(DFS)回溯函数
     * @param i 当前处理的行号(0到n-1)
     */
    static void dfs(int i, int n, char[][] table, boolean[] ca, 
                    boolean[] cb, boolean[] cc, List<List<String>> result) {
        
        // 终止条件:所有行都已放置皇后
        if (i == n) {
            // 将当前棋盘状态转换为字符串列表
            List<String> list = new ArrayList<>();
            for (char[] chars : table) {
                list.add(new String(chars));
            }
            result.add(list);
            return;
        }
        
        // 遍历当前行的每一列,尝试放置皇后
        for (int j = 0; j < n; j++) {
            // ===== 冲突检测 =====
            // 1. ca[j]: 第j列是否已有皇后
            // 2. cb[i+j]: 该左斜线(↘)是否已有皇后
            // 3. cc[n-1-(i-j)]: 该右斜线(↙)是否已有皇后
            if (ca[j] || cb[i + j] || cc[n - 1 - (i - j)]) {
                continue;  // 跳过冲突列
            }
            
            // ===== 做选择 =====
            table[i][j] = 'Q';  // 放置皇后
            ca[j] = cb[i + j] = cc[n - 1 - (i - j)] = true;  // 标记冲突
            
            // ===== 递归 =====
            dfs(i + 1, n, table, ca, cb, cc, result);  // 处理下一行
            
            // ===== 撤销选择(回溯)=====
            table[i][j] = '.';  // 移除皇后
            ca[j] = cb[i + j] = cc[n - 1 - (i - j)] = false;  // 清除冲突标记
        }
    }
}

5. 复杂度分析

5.1 时间复杂度:O(n!)

理论上界为 n!,因为:

  • 第1行有 n 种选择
  • 第2行最多 n-1 种选择
  • 第3行最多 n-2 种选择
  • ...
  • 总共约 n × (n-1) × (n-2) × ... × 1 = n! 种可能

5.2 空间复杂度

组成部分 空间 说明
递归调用栈 O(n) 最大递归深度为n
冲突检测数组 O(n) 三个数组大小分别为n, 2n-1, 2n-1
棋盘 O(n²) n×n的字符数组
总空间 O(n²) 主要由棋盘决定

6. 优化技巧

6.1 位运算优化

使用位运算代替布尔数组,可大幅减少空间占用:

  • 用一个整数的每一位表示冲突状态
  • 利用位运算快速判断和更新状态
  • 空间复杂度可从O(n)降至O(1)

6.2 对称性剪枝

  • n皇后问题的解具有对称性
  • 只搜索一半的解,然后通过对称变换得到另一半
  • 可减少约50%的搜索空间

6.3 启发式搜索

  • 优先选择约束最多的位置(可放置皇后最少的行)
  • 减少回溯次数,提高效率

7. 相关问题推荐

问题编号 问题名称 关联度 说明
LeetCode 52 N皇后 II ★★★★★ 只求解的数量,不需输出具体方案
LeetCode 37 解数独 ★★★★☆ 二维回溯问题,约束更复杂
LeetCode 980 不同路径 III ★★★☆☆ 回溯+路径搜索,状态管理类似

8. 总结

8.1 核心要点

  1. 逐行放置:自动避免行冲突,降低问题复杂度
  2. 三数组检测 :使用ca, cb, cc实现O(1)冲突检测
  3. 斜线索引 :理解i+jn-1-(i-j)的映射关系是关键
  4. 回溯思想:递归+状态恢复,枚举所有可能解
相关推荐
野生风长6 分钟前
c++类和对象(this指针,重载operator,习题总结)
java·开发语言·c++
最后的最后、7968 分钟前
SQLi-Labs 六关 SQL 注入过关笔记
数据库·笔记·sql
zander25814 分钟前
138. 随机链表的复制
数据结构·算法·链表
霸道流氓气质16 分钟前
基于 Spring 事务同步机制的事务后置动作收集器 Starter 实践
java·后端·spring
筱谙18 分钟前
BES BLE CTKD 完整学习笔记
笔记·学习
会编程的土豆21 分钟前
数据库mysql八股
数据库·笔记·八股
过期动态22 分钟前
【LeetCode 热题 100】找到字符串中所有字母异位词
java·数据结构·算法·leetcode·职场和发展·rabbitmq
数据皮皮侠AI22 分钟前
退市监督数据(2000-2024)
大数据·人工智能·笔记·机器学习·回归
来一碗刘肉面31 分钟前
栈在递归中的应用
数据结构·算法