N皇后问题:经典回溯算法的一些分析

NNN皇后问题是计算机科学和算法设计中的一个经典问题。给定一个大小为 N×NN \times NN×N 的棋盘,我们需要在棋盘上放置 NNN 个皇后,使得它们彼此之间不能相互攻击(即任意两个皇后不能在同一行、同一列或同一对角线上)。本文将详细解析一个C++解决方案,并探讨其背后的算法思想。

1 问题描述

NNN皇后问题的核心约束条件:

  1. 每行只能放置一个皇后
  2. 每列只能放置一个皇后
  3. 每条对角线上只能放置一个皇后

2 解决方案概览

我们采用回溯算法(Backtracking)来解决这个问题。回溯算法是一种通过探索所有可能的选择来寻找所有解的算法,当发现当前选择无法得到有效解时,会"回溯"到上一步,尝试其他选择。

3 完整代码

cpp 复制代码
class Solution
{
private:
    int n;
    vector<int> queens_r2c; // 根据皇后的行号给出对应的列号,如果还未放好值就是-1
    unordered_set<int> busy_columns; // 已经被占用的列
    unordered_set<int> busy_diagonals1; // 已经被占用的对角线1
    unordered_set<int> busy_diagonals2; // 已经被占用的对角线2
    vector<vector<string>> answer;

    vector<string> generateBoard()
    {
        vector<string> board;
        for (int r = 0; r < n; r++)
        {
            string temp = string(n, '.');
            int c = queens_r2c[r];
            temp[c] = 'Q';
            board.emplace_back(temp);
        }
        return board;
    }

    void backtrack(int r)
    {
        if (r == n)
        {
            vector<string> result = generateBoard();
            answer.emplace_back(result);
            return;
        }
        else
        {
            for (int c = 0; c < n; c++)
            {
                if (busy_columns.end() != busy_columns.find(c))
                {
                    continue;
                }
                int diagonals1 = r - c;
                if (busy_diagonals1.end() != busy_diagonals1.find(diagonals1))
                {
                    continue;
                }
                int diagonals2 = r + c;
                if (busy_diagonals2.end() != busy_diagonals2.find(diagonals2))
                {
                    continue;
                }

                queens_r2c[r] = c;
                busy_columns.insert(c);
                busy_diagonals1.insert(diagonals1);
                busy_diagonals2.insert(diagonals2);
                backtrack(r + 1);
                queens_r2c[r] = -1;
                busy_columns.erase(c);
                busy_diagonals1.erase(diagonals1);
                busy_diagonals2.erase(diagonals2);
            }
        }
        return;
    }
public:
    vector<vector<string>> solveNQueens(int n)
    {
        this->n = n;
        this->queens_r2c.resize(this->n, -1);
        backtrack(0);
        return this->answer;
    }
};

4 核心数据结构

cpp 复制代码
class Solution {
private:
    int n;                                // 棋盘大小
    vector<int> queens_r2c;               // 行号到列号的映射
    unordered_set<int> busy_columns;      // 已占用的列
    unordered_set<int> busy_diagonals1;   // 已占用的主对角线
    unordered_set<int> busy_diagonals2;   // 已占用的副对角线
    vector<vector<string>> answer;        // 存储所有解
};

// ...

}

5 关键技巧:对角线标识

这是本算法的亮点所在。我们使用简单的数学公式来唯一标识每条对角线:

  • 主对角线(左上到右下)r - c

    • 在同一条主对角线上的所有格子,行号减列号的值相同
    • 例如:(0,0)(0,0)(0,0),(1,1)(1,1)(1,1),(2,2)(2,2)(2,2)的差值都是000
  • 副对角线(右上到左下)r + c

    • 在同一条副对角线上的所有格子,行号加列号的值相同
    • 例如:(0,2)(0,2)(0,2),(1,1)(1,1)(1,1),(2,0)(2,0)(2,0)的和都是222

这种标识方法使得我们可以在常数时间内检查一个位置是否在对角线冲突。

6 算法流程详解

6.1 初始化

cpp 复制代码
this->n = n;
this->queens_r2c.resize(this->n, -1);  // 初始化为-1,表示未放置
backtrack(0);  // 从第0行开始回溯

6.2 回溯核心函数

cpp 复制代码
void backtrack(int r) {
    // 终止条件:所有行都已放置皇后
    if (r == n) {
        vector<string> result = generateBoard();
        answer.emplace_back(result);
        return;
    }
    
    // 遍历当前行的所有列
    for (int c = 0; c < n; c++) {
        // 检查列冲突
        if (busy_columns.find(c) != busy_columns.end()) continue;
        
        // 检查主对角线冲突
        int diag1 = r - c;
        if (busy_diagonals1.find(diag1) != busy_diagonals1.end()) continue;
        
        // 检查副对角线冲突
        int diag2 = r + c;
        if (busy_diagonals2.find(diag2) != busy_diagonals2.end()) continue;
        
        // 做出选择
        queens_r2c[r] = c;
        busy_columns.insert(c);
        busy_diagonals1.insert(diag1);
        busy_diagonals2.insert(diag2);
        
        // 递归到下一行
        backtrack(r + 1);
        
        // 撤销选择(回溯)
        queens_r2c[r] = -1;
        busy_columns.erase(c);
        busy_diagonals1.erase(diag1);
        busy_diagonals2.erase(diag2);
    }
}

3. 生成棋盘表示

cpp 复制代码
vector<string> generateBoard() {
    vector<string> board;
    for (int r = 0; r < n; r++) {
        string temp = string(n, '.');   // 创建一行,初始为'.'
        int c = queens_r2c[r];          // 获取该行皇后的列位置
        temp[c] = 'Q';                  // 放置皇后
        board.emplace_back(temp);
    }
    return board;
}

算法复杂度分析

时间复杂度

  • 最坏情况下需要探索所有可能的放置方式:O(N!)O(N!)O(N!)
  • 但由于剪枝(提前终止无效分支),实际运行时间远小于N!N!N!

空间复杂度

  • 递归栈深度:O(N)O(N)O(N)
  • 存储解的空间:O(N2)O(N^2)O(N2),每个解需要存储一个N×NN \times NN×N的棋盘
  • 辅助数据结构:O(N)O(N)O(N),用于存储已占用的列和对角线

算法优化点

  1. 使用哈希集合unordered_set提供O(1)O(1)O(1)的查找、插入和删除操作
  2. 行号到列号的映射queens_r2c数组直接记录每行皇后的位置,避免了二维数组的开销
  3. 对角线标识技巧:使用简单的数学公式快速判断对角线冲突

实际应用场景

NNN皇后问题虽然是一个理论问题,但其解决方案在很多实际场景中都有应用:

  • 调度问题:安排任务避免冲突
  • 电路板设计:避免信号干扰
  • 数据库设计:优化数据存储和查询

扩展思考

  1. 如何只求一个解而不是所有解?
  2. 如何优化算法求最大N值?
  3. 如何并行化回溯算法?

总结

NNN皇后问题的回溯解法展示了如何通过巧妙的剪枝和数据结构设计,将一个看似复杂的问题转化为可高效求解的算法。对角线标识的技巧尤其值得学习,它体现了将几何关系转化为代数关系的思维方式。

这种算法设计思想在解决其他约束满足问题(如数独、图着色等)时同样适用,是每个算法学习者都应该掌握的核心技能。

相关推荐
Wect2 小时前
LeetCode 530. 二叉搜索树的最小绝对差:两种解法详解(迭代+递归)
前端·算法·typescript
Rabbit_QL2 小时前
【BPE实战】从零实现 BPE 分词器:训练、编码与解码
python·算法·nlp
小O的算法实验室2 小时前
2024年IEEE TII SCI1区TOP,面向动态多目标多AUV路径规划的协同进化计算算法,深度解析+性能实测
算法·论文复现·智能算法·智能算法改进
Charlie_lll2 小时前
力扣解题-88. 合并两个有序数组
后端·算法·leetcode
菜鸡儿齐3 小时前
leetcode-最小栈
java·算法·leetcode
雪人不是菜鸡3 小时前
简单工厂模式
开发语言·算法·c#
岛雨QA3 小时前
常用十种算法「Java数据结构与算法学习笔记13」
数据结构·算法
weiabc3 小时前
printf(“%lf“, ys) 和 cout << ys 输出的浮点数格式存在细微差异
数据结构·c++·算法
铸人3 小时前
大数分解的Shor算法-C#
开发语言·算法·c#