八皇后问题是回溯算法的经典案例,由国际象棋棋手马克斯・贝瑟尔于 1848 年提出。问题要求在 8×8 的棋盘上放置 8 个皇后,使它们不能互相攻击(即任意两个皇后不能处于同一行、同一列或同一斜线上)。作为回溯算法的标杆问题,八皇后不仅是 LeetCode 的高频考题,也是考研计算机专业基础综合(408)的重点考察内容。
八皇后问题核心思路
问题定义与约束
在n×n的棋盘上放置n个皇后,满足以下约束:
- 任意两个皇后不在同一行。
- 任意两个皇后不在同一列。
- 任意两个皇后不在同一斜线上(包括主对角线和副对角线)。
以 8 皇后为例,其解共有 92 种,每种解对应皇后在棋盘上的一种合法布局。
回溯算法思路
回溯算法的核心是 “尝试 - 检测 - 回溯”:通过递归尝试放置皇后,若违反约束则回溯到上一步,调整位置后继续尝试,直至找到所有合法解。具体步骤如下:
- 按行放置:由于皇后不能在同一行,可按行依次放置(第i个皇后放在第i行),只需考虑列和对角线约束。
- 列冲突检测:记录已放置皇后的列索引,若当前列已被占用则冲突。
- 对角线冲突检测:对于第i行第j列的皇后,与第k行第l列的皇后,若|i - k| == |j - l|则在同一斜线,冲突。
- 递归回溯:
-
- 若已放置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 皇后问题的所有解,并计算算法的时间复杂度。要求用最少的空间存储约束信息。
解题思路
- 空间优化:
-
- 无需存储整个棋盘,只需记录每一行皇后的列索引(queens[row] = col)。
-
- 列冲突:检查col是否在queens[0..row-1]中。
-
- 对角线冲突:对已放置的皇后(k, queens[k]),检查|row - k| == |col - queens[k]|。
- 算法步骤:
-
- 递归函数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 皇后问题的递归实现、冲突检测逻辑。
- 重点掌握:
- 如何用最少的空间记录约束信息(如仅用数组记录列索引)。
- 时间复杂度分析(阶乘级复杂度的推导)。
- 回溯与递归的关系,以及回溯过程中状态的恢复。
- 常见错误:忽略对角线冲突的双向性(主对角线和副对角线),或未正确恢复回溯状态。
总结
八皇后问题作为回溯算法的经典案例,其核心思想 “尝试 - 检测 - 回溯” 是解决组合优化问题的重要范式。
掌握八皇后问题的关键在于:
- 理解冲突检测的逻辑(列和对角线约束)。
- 熟练运用递归实现回溯,正确处理状态的保存与恢复。
- 掌握空间优化技巧,减少不必要的内存开销。
在考研备考中,需重点关注回溯算法的时间复杂度分析和状态管理,这不仅有助于解决 n 皇后问题,也能迁移到其他回溯类问题(如子集、排列、迷宫求解等)。
希望本文能够帮助读者更深入地理解回溯算法中活动选择问题算法,并在实际项目中发挥其优势。谢谢阅读!
希望这份博客能够帮助到你。如果有其他需要修改或添加的地方,请随时告诉我。