【Hot 100 刷题计划】 LeetCode 51. N 皇后 | C++ 回溯算法&状态数组

LeetCode 51. N 皇后

📌 题目描述

题目级别:困难

按照国际象棋的规则,皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。
n 皇后问题 研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。

给你一个整数 n ,返回所有不同的 n 皇后问题 的解决方案。

每一种解法包含一个不同的 n 皇后问题 的棋子放置方案,该方案中 'Q''.' 分别代表了皇后和空位。


💡 解法一:DFS 逐行放置与绝对值校验

这是回溯算法的最经典应用。我们不需要在一个 N × N N \times N N×N 的格子里一个个盲目去试,因为题目明确规定"同行不能有皇后"。

因此,我们可以按行进行 DFS 递归

  1. 每一层递归负责在当前行 u 中找一个合适的列 i 放置皇后。
  2. 放置前,通过 check 函数校验是否合法。
  3. 这里的校验逻辑非常具有几何直觉:
    • 同一列不冲突:j != y
    • 同一斜线不冲突:利用斜率为 1 和 -1 的直线特性,如果两个点在同一条斜线上,它们的横坐标之差的绝对值必然等于纵坐标之差的绝对值,即 abs(i - x) != abs(j - y)

💻 C++ 代码实现 (精简修正版)

cpp 复制代码
class Solution {
public:
    vector<vector<string>> res;
    vector<string> tmp;

    vector<vector<string>> solveNQueens(int n) {
        // 优雅初始化:生成一个 n 行,每行包含 n 个 '.' 的字符串数组
        tmp = vector<string>(n, string(n, '.'));
        
        // 从第 0 行开始深度优先搜索
        dfs(0, n);
        return res;
    }

    void dfs(int u, int n)
    {
        // 递归终止条件:已经成功放置到了第 n 行(0 到 n-1 都放好了)
        if (u == n)
        {
            res.push_back(tmp);
            return ;
        }

        // 尝试在当前行 u 的每一列 i 放置皇后
        for (int i = 0; i < n; i ++ )
        {
            if (check(u, i, n))
            {
                tmp[u][i] = 'Q';     // 处理节点
                dfs(u + 1, n);       // 向下递归,处理下一行
                tmp[u][i] = '.';     // 回溯:撤销处理,尝试下一列
            }
        }
    }

    // 校验在 (x, y) 放置皇后是否合法
    bool check(int x, int y, int n)
    {
        // 只需要检查 x 行之前的行即可,因为下面的行还没放皇后
        for (int i = 0; i < x; i ++ )
        {
            for (int j = 0; j < n; j ++ )
            {
                if (tmp[i][j] == 'Q')
                {
                    // 检查列冲突 或 对角线冲突
                    if (j == y || abs(i - x) == abs(j - y)) return false;
                }
            }
        }
        return true;
    }
};

💡 解法二:状态数组降维打击

在基础版本中,每次放置皇后都要回头遍历棋盘检查冲突,校验的时间复杂度是 O ( N 2 ) O(N^2) O(N2)。

为了追求极致性能,我们可以开辟三个布尔数组,专门记录哪些列、哪些斜线已经被占用了:

  1. col 数组 :记录哪一列有皇后。col[i] = true 表示第 i 列被占。
  2. dg 数组(正对角线) :观察棋盘发现,处于同一条正对角线(右上到左下, /)上的点,它们的行号和列号之和是固定的x + y = const)。我们用 dg[u + i] 来标记这条斜线。
  3. udg 数组(反对角线) :处于同一条反对角线(左上到右下, \)上的点,它们的行号和列号之差是固定的y - x = const)。为了防止数组下标出现负数,我们加上一个偏移量 n,即用 udg[n - u + i] 来标记这条斜线。

有了这三个数组,判断一个点能不能放皇后,只需要查三个布尔值即可,瞬间降维到 O ( 1 ) O(1) O(1)!


💻 C++ 代码实现 (极客优化版)

cpp 复制代码
class Solution {
private:
    vector<vector<string>> res;
    vector<string> board;
    // 状态数组:因为是对角线,最大数量是 2N-1,所以开大一点 20 足够应付 n=9
    bool col[20], dg[20], udg[20]; 

    void dfs(int u, int n) {
        // 成功放置到最后一行,收集结果
        if (u == n) {
            res.push_back(board);
            return;
        }

        // 尝试在第 u 行的每一列 i 放置皇后
        for (int i = 0; i < n; i++) {
            // O(1) 极速校验:该列、该正对角线、该反对角线均没有被占用
            if (!col[i] && !dg[u + i] && !udg[n - u + i]) {
                
                board[u][i] = 'Q';
                // 同步更新三个状态数组,宣告占领
                col[i] = dg[u + i] = udg[n - u + i] = true;
                
                dfs(u + 1, n); // 向下递归
                
                // 回溯:撤销皇后,并释放三个状态数组的占领
                col[i] = dg[u + i] = udg[n - u + i] = false;
                board[u][i] = '.';
            }
        }
    }

public:
    vector<vector<string>> solveNQueens(int n) {
        board.assign(n, string(n, '.'));
        // 初始化状态数组
        memset(col, false, sizeof col);
        memset(dg, false, sizeof dg);
        memset(udg, false, sizeof udg);
        
        dfs(0, n);
        return res;
    }
};
相关推荐
脱氧核糖核酸__2 小时前
LeetCode热题100——41.缺失的第一个正数(题解+答案+要点)
数据结构·c++·算法·leetcode·哈希算法
脱氧核糖核酸__2 小时前
LeetCode热题100——73.矩阵置零(题目+题解+答案)
c++·算法·leetcode·矩阵
Mr_Xuhhh2 小时前
深入理解单链表的递归反转:从原理到实现
算法·leetcode·职场和发展
智者知已应修善业2 小时前
【51单片机数码管+蜂鸣器的使用】2023-6-14
c++·经验分享·笔记·算法·51单片机
艾莉丝努力练剑2 小时前
【Linux线程】Linux系统多线程(七):<线程同步与互斥>线程同步(下)
java·linux·运维·服务器·c++·学习·操作系统
迷途之人不知返2 小时前
算法类型:双指针类型
算法
c++逐梦人2 小时前
C++ RAII流式日志库实现
开发语言·c++
吴可可1232 小时前
三点绘圆弧的几何实现
算法
t***5442 小时前
还有哪些设计模式适合现代C++
开发语言·c++·设计模式