算法学习笔记:24.回溯算法之八皇后问题 ——从原理到实战,涵盖 LeetCode 与考研 408 例题

八皇后问题是回溯算法的经典案例,由国际象棋棋手马克斯・贝瑟尔于 1848 年提出。问题要求在 8×8 的棋盘上放置 8 个皇后,使它们不能互相攻击(即任意两个皇后不能处于同一行、同一列或同一斜线上)。作为回溯算法的标杆问题,八皇后不仅是 LeetCode 的高频考题,也是考研计算机专业基础综合(408)的重点考察内容。


八皇后问题核心思路

问题定义与约束

在n×n的棋盘上放置n个皇后,满足以下约束:

  • 任意两个皇后不在同一行。
  • 任意两个皇后不在同一列。
  • 任意两个皇后不在同一斜线上(包括主对角线和副对角线)。

以 8 皇后为例,其解共有 92 种,每种解对应皇后在棋盘上的一种合法布局。

 回溯算法思路

回溯算法的核心是 “尝试 - 检测 - 回溯”:通过递归尝试放置皇后,若违反约束则回溯到上一步,调整位置后继续尝试,直至找到所有合法解。具体步骤如下:

  1. 按行放置:由于皇后不能在同一行,可按行依次放置(第i个皇后放在第i行),只需考虑列和对角线约束。
  1. 列冲突检测:记录已放置皇后的列索引,若当前列已被占用则冲突。
  1. 对角线冲突检测:对于第i行第j列的皇后,与第k行第l列的皇后,若|i - k| == |j - l|则在同一斜线,冲突。
  1. 递归回溯
    • 若已放置n个皇后(到达最后一行),记录当前解。
    • 否则,对当前行的每一列尝试放置皇后:
      • 若不冲突,标记列和对角线为占用,递归处理下一行。
      • 递归返回后,撤销标记(回溯),尝试下一列。

算法流程图

(以 4 皇后为例,展示部分过程)

LeetCode例题实战

例题1:51. N 皇后(困难)

题目描述:按照国际象棋的规则,摆放`n`个皇后,使它们不能相互攻击。给你一个整数`n`,返回所有不同的`n`皇后问题的解决方案。每个解决方案包含一个不同的`n`皇后问题的棋子放置方案,该方案中`'Q'`和`'.'`分别代表皇后和空位。

示例

输入:n = 4

输出:[[".Q..","...Q","Q...","..Q."],

["..Q.","Q...","...Q",".Q.."]]

解题思路

- `board`:二维数组表示棋盘,记录皇后位置。

- `columns`:哈希集记录已占用的列。

- `diagonals1`:哈希集记录已占用的主对角线(`行-列`为定值)。

- `diagonals2`:哈希集记录已占用的副对角线(`行+列`为定值)。

2. **递归函数**:`backtrack(row)`表示处理第`row`行的皇后放置。

3. **终止条件**:当`row == n`时,将当前棋盘转为字符串列表,加入结果集。

4. **尝试放置**:对当前行的每一列`col`:

- 若`col`、`row-col`、`row+col`均不在对应集合中,说明无冲突。

- 放置皇后,更新集合,递归处理下一行。

- 回溯:移除皇后,恢复集合状态。

代码实现
import java.util.*;

class Solution {

    private List<List<String>> result = new ArrayList<>();

    private Set<Integer> columns = new HashSet<>();

    private Set<Integer> diagonals1 = new HashSet<>();

    private Set<Integer> diagonals2 = new HashSet<>();

    private int n;

    public List<List<String>> solveNQueens(int n) {

        this.n = n;

        char[][] board = new char[n][n];

        for (char[] row : board) {

            Arrays.fill(row, '.');

        }

        backtrack(board, 0);

        return result;

    }

    private void backtrack(char[][] board, int row) {

// 终止条件:所有行都放置了皇后

        if (row == n) {

            result.add(convert(board));

            return;

        }

        for (int col = 0; col < n; col++) {

// 计算对角线标识

            int d1 = row - col;

            int d2 = row + col;

// 检测冲突

            if (columns.contains(col) || diagonals1.contains(d1) || diagonals2.contains(d2)) {

                continue;

            }

// 放置皇后

            board[row][col] = 'Q';

            columns.add(col);

            diagonals1.add(d1);

            diagonals2.add(d2);

// 递归处理下一行

            backtrack(board, row + 1);

// 回溯:撤销放置

            board[row][col] = '.';

            columns.remove(col);

            diagonals1.remove(d1);

            diagonals2.remove(d2);

        }

    }

// 将棋盘转为字符串列表

    private List<String> convert(char[][] board) {

        List<String> list = new ArrayList<>();

        for (char[] row : board) {

            list.add(new String(row));

        }

        return list;

    }

}
复杂度分析
  • 时间复杂度:O (n!),第 1 行有 n 种选择,第 2 行最多 n-2 种(排除列和对角线),总次数约为 n!。
  • 空间复杂度:O (n),递归栈深度为 n,集合存储的约束信息最多为 O (n)。

例题 2:52. N 皇后 II(困难)

题目描述:n 皇后问题研究的是如何将 n 个皇后放置在 n×n 的棋盘上,并且使皇后彼此之间不能相互攻击。给你一个整数 n ,返回 n 皇后问题不同的解决方案的数量。

示例

输入:n = 4

输出:2

解题思路

与 51 题思路一致,只需将 “记录解决方案” 改为 “计数解决方案数量”,可优化空间(用数组代替哈希集)。

代码实现
class Solution {

    private int count = 0;

    private int n;

    public int totalNQueens(int n) {

        this.n = n;

// 用数组记录约束,索引为列/对角线值,值为是否占用

        boolean[] columns = new boolean[n];

        boolean[] diagonals1 = new boolean[2 * n - 1]; // 主对角线:row-col 范围为-(n-1)~n-1,偏移n-1

        boolean[] diagonals2 = new boolean[2 * n - 1]; // 副对角线:row+col 范围为0~2n-2

        backtrack(0, columns, diagonals1, diagonals2);

        return count;

    }

    private void backtrack(int row, boolean[] columns, boolean[] diagonals1, boolean[] diagonals2) {

        if (row == n) {

            count++;

            return;

        }

        for (int col = 0; col < n; col++) {

            int d1 = row - col + n - 1; // 主对角线偏移,确保非负

            int d2 = row + col;

            if (columns[col] || diagonals1[d1] || diagonals2[d2]) {

                continue;

            }

// 放置皇后

            columns[col] = true;

            diagonals1[d1] = true;

            diagonals2[d2] = true;

            backtrack(row + 1, columns, diagonals1, diagonals2);

// 回溯

            columns[col] = false;

            diagonals1[d1] = false;

            diagonals2[d2] = false;

        }

    }

}
复杂度分析
  • 时间复杂度:O (n!),与 51 题相同。
  • 空间复杂度:O (n),数组存储约束信息,递归栈深度为 n。

考研 408 例题解析

例题 1:概念辨析题(选择题)

题目:关于八皇后问题的回溯算法,下列说法错误的是( )。

A. 算法的时间复杂度为 O (n!)

B. 空间复杂度主要来自递归栈和约束记录

C. 回溯过程中需要检测行、列和对角线冲突

D. 算法通过贪心选择减少无效尝试

答案:D

解析

  • A 正确:n 皇后问题的时间复杂度为 O (n!),随 n 呈阶乘增长。
  • B 正确:空间复杂度为 O (n),包括递归栈(深度 n)和约束集合(大小 O (n))。
  • C 正确:八皇后需检测行(隐含在按行放置中)、列和对角线冲突。
  • D 错误:八皇后问题使用回溯算法,而非贪心算法。贪心算法难以保证找到所有解,而回溯会枚举所有可能。

例题 2:算法设计题(408 高频考点)

题目:设计一个回溯算法,求解 n 皇后问题的所有解,并计算算法的时间复杂度。要求用最少的空间存储约束信息。

解题思路
  1. 空间优化
    • 无需存储整个棋盘,只需记录每一行皇后的列索引(queens[row] = col)。
    • 列冲突:检查col是否在queens[0..row-1]中。
    • 对角线冲突:对已放置的皇后(k, queens[k]),检查|row - k| == |col - queens[k]|。
  1. 算法步骤
    • 递归函数backtrack(row):处理第row行。
    • 对每列col,检查无冲突后放置皇后,递归处理row+1。
    • 回溯时恢复queens[row]的状态。
代码实现(空间优化版)
import java.util.*;

public class NQueensOptimized {

    private List<List<String>> result = new ArrayList<>();

    private int[] queens; // 记录每行皇后的列索引

    private int n;

    public List<List<String>> solveNQueens(int n) {

        this.n = n;

        queens = new int[n];

        Arrays.fill(queens, -1); // -1表示未放置

        backtrack(0);

        return result;

    }

    private void backtrack(int row) {

        if (row == n) {

            result.add(generateBoard());

            return;

        }

        for (int col = 0; col < n; col++) {

            if (isValid(row, col)) {

                queens[row] = col;

                backtrack(row + 1);

                queens[row] = -1; // 回溯

            }

        }

    }

// 检查第row行第col列是否可放置皇后

    private boolean isValid(int row, int col) {

        for (int k = 0; k < row; k++) {

            int placedCol = queens[k];

// 列冲突或对角线冲突

            if (placedCol == col || Math.abs(row - k) == Math.abs(col - placedCol)) {

                return false;

            }

        }

        return true;

    }

// 根据queens数组生成棋盘字符串

    private List<String> generateBoard() {

        List<String> board = new ArrayList<>();

        for (int i = 0; i < n; i++) {

            char[] row = new char[n];

            Arrays.fill(row, '.');

            row[queens[i]] = 'Q';

            board.add(new String(row));

        }

        return board;

    }

    public static void main(String[] args) {

        NQueensOptimized solver = new NQueensOptimized();

        System.out.println(solver.solveNQueens(4));

    }

}

时间复杂度分析
  • 时间复杂度:O (n!)。第 1 行有 n 种选择,第 2 行最多 n-2 种(排除列和对角线),第 3 行最多 n-4 种,总次数约为 n!。
  • 空间复杂度:O (n),包括queens数组(大小 n)和递归栈(深度 n),为最优空间复杂度。

八皇后问题的扩展与应用

实际应用场景

  • 调度问题:如任务调度中避免资源冲突,类似皇后避免冲突的约束。
  • 布局优化:电路板元件布局、卫星轨道设计等,需避免元件 / 卫星间的干扰。
  • 算法思想迁移:回溯算法可用于子集生成、排列组合、迷宫求解等问题。

变种问题

  • n 皇后计数:如 LeetCode 52 题,只需返回解的数量。
  • 多皇后问题:在m×n棋盘上放置k个皇后,约束更复杂。
  • 有障碍的 n 皇后:棋盘上有障碍物,皇后不能放在障碍上。

 考研 408 备考要点

  • 核心考点:回溯算法的基本思想、n 皇后问题的递归实现、冲突检测逻辑。
  • 重点掌握
  1. 如何用最少的空间记录约束信息(如仅用数组记录列索引)。
  2. 时间复杂度分析(阶乘级复杂度的推导)。
  3. 回溯与递归的关系,以及回溯过程中状态的恢复。
  • 常见错误:忽略对角线冲突的双向性(主对角线和副对角线),或未正确恢复回溯状态。

总结

八皇后问题作为回溯算法的经典案例,其核心思想 “尝试 - 检测 - 回溯” 是解决组合优化问题的重要范式。

掌握八皇后问题的关键在于:

  1. 理解冲突检测的逻辑(列和对角线约束)。
  2. 熟练运用递归实现回溯,正确处理状态的保存与恢复。
  3. 掌握空间优化技巧,减少不必要的内存开销。

在考研备考中,需重点关注回溯算法的时间复杂度分析和状态管理,这不仅有助于解决 n 皇后问题,也能迁移到其他回溯类问题(如子集、排列、迷宫求解等)。

希望本文能够帮助读者更深入地理解回溯算法中活动选择问题算法,并在实际项目中发挥其优势。谢谢阅读!


希望这份博客能够帮助到你。如果有其他需要修改或添加的地方,请随时告诉我。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

呆呆企鹅仔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值