八皇后问题是计算机科学中最经典的回溯算法教学案例之一。传统的解法依赖递归回溯与位运算剪枝,本文将换一个完全不同的视角——用模拟退火(Simulated Annealing)这一受物理退火过程启发的随机优化算法来求解。读者将理解如何将组合优化问题转化为能量最小化模型,并掌握一种在巨大解空间中寻找近似最优解的通用技术。
一、问题回顾:八皇后挑战
在 8×8 的国际象棋棋盘上放置 8 个皇后,使得任意两个皇后都不能互相攻击。皇后的攻击范围涵盖同一行、同一列以及同一对角线。本质上,我们需要找到一组皇后的坐标,使得所有皇后两两之间满足约束条件。
八皇后问题的解空间极其庞大:若不做任何限制,共有 $64^8 \approx 2.8 \times 10^{14}$ 种放置方式。但通过观察可知,最优解中每行每列恰好只有一个皇后,因此可将搜索空间压缩为 $8! = 40320$ 种排列——这已是模拟退火可以轻松处理的范围。
二、模拟退火算法原理
模拟退火借鉴了金属热处理的物理过程:高温下金属原子剧烈运动,随着温度缓慢降低,原子逐渐趋于有序,最终形成能量最低的稳定晶体结构。在算法中,这一过程对应为:
- 状态(State):问题的一个候选解,对应八皇后问题中的一种棋盘布局。
- 能量(Energy):衡量状态优劣的指标,能量越低表示解越好。对于八皇后问题,能量定义为棋盘上互相攻击的皇后对数。
- 温度(Temperature):控制随机性的参数。高温时算法倾向于探索,接受劣质解的概率较高;低温时趋于稳定,主要围绕优质解微调。
- 邻域操作(Neighbor):在当前状态附近产生一个新状态。八皇后问题中,我们随机交换两列中皇后的行号。
- Metropolis 准则:决定是否接受新状态。若新状态能量更低,则一定接受;若能量更高,则以概率 $P = e^{-\Delta E / T}$ 接受,其中 $\Delta E$ 为能量差,$T$ 为当前温度。
温度按指数速率衰减:$T_{k+1} = \alpha \cdot T_k$($0 < \alpha < 1$)。当温度趋近于零时,算法退化为普通的贪婪下降,最终收敛到局部最优解。
三、八皇后的模拟退火模型设计
3.1 状态编码
使用一维数组 board[col] 表示第 col 列的皇后所在的行号。由于每列恰好一个皇后,数组长度为 8。为了进一步缩小搜索空间,我们强制每行也恰好只有一个皇后,即 board 是 $[0, 7]$ 的一个排列。这样能量函数只需统计对角线冲突:
$$
E = \sum_{i=0}^{6} \sum_{j=i+1}^{7} \mathbb{1}\left[ |board[i] – board[j]| = |i – j| \right]
$$
3.2 邻域生成
在排列空间上,最自然的邻域操作是随机交换两个位置:
随机选择 i, j ∈ [0, 7],交换 board[i] 与 board[j]
该操作保证新状态仍然满足每行每列唯一的约束。
3.3 退火 schedule
初始温度 $T_0 = 10.0$,衰减系数 $\alpha = 0.999$,终止条件为 $T < 10^{-6}$ 或能量降为零。为了提升成功率,当退火未能找到零能量解时,重新随机初始化并再次执行退火(重启策略)。
四、Java 实现:核心数据结构
首先定义棋盘类,封装状态、能量计算与邻域生成:
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
/**
* 八皇后问题的模拟退火求解器
* 状态编码:board[col] 表示第 col 列皇后的行号,且为 0~7 的排列
*/
public class NQueensSimulatedAnnealing {
private final int n; // 棋盘大小(皇后数量)
private final int[] board; // 当前状态,排列表示
private int currentEnergy; // 当前状态的能量(对角线冲突对数)
// 随机数生成器(避免并发问题,每个实例独立)
private final java.util.Random random = new java.util.Random();
public NQueensSimulatedAnnealing(int n) {
this.n = n;
this.board = new int[n];
}
/**
* 随机初始化:生成 0~n-1 的随机排列
* 保证每行每列恰好一个皇后
*/
private void randomInit() {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < n; i++) {
list.add(i);
}
Collections.shuffle(list, random);
for (int i = 0; i < n; i++) {
board[i] = list.get(i);
}
currentEnergy = computeEnergy();
}
/**
* 计算当前状态的能量:对角线冲突的皇后对数
* 由于状态是排列,行冲突和列冲突天然为零
*/
private int computeEnergy() {
int attacks = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
int rowDiff = Math.abs(board[i] - board[j]);
int colDiff = Math.abs(i - j);
if (rowDiff == colDiff) {
attacks++;
}
}
}
return attacks;
}
/**
* 生成邻域状态:随机交换两列皇后的行号
* 返回能量变化量 delta = newEnergy - currentEnergy
*/
private int generateNeighbor() {
int i = random.nextInt(n);
int j = random.nextInt(n);
while (i == j) {
j = random.nextInt(n);
}
// 交换前记录,以便必要时恢复
int oldI = board[i];
int oldJ = board[j];
// 计算与位置 i 和 j 相关的冲突变化(增量更新,避免 O(n^2) 重算)
int oldContribution = contribution(i) + contribution(j);
// 执行交换
board[i] = oldJ;
board[j] = oldI;
int newContribution = contribution(i) + contribution(j);
int delta = newContribution - oldContribution;
currentEnergy += delta;
return delta;
}
/**
* 计算某一列皇后与其他所有列皇后的对角线冲突数
*/
private int contribution(int col) {
int count = 0;
for (int k = 0; k < n; k++) {
if (k == col) continue;
if (Math.abs(board[col] - board[k]) == Math.abs(col - k)) {
count++;
}
}
return count;
}
/**
* 撤销上一次交换操作(当新状态被拒绝时)
*/
private void undoSwap(int i, int j) {
int temp = board[i];
board[i] = board[j];
board[j] = temp;
currentEnergy = computeEnergy(); // 安全起见完整重算
}
五、Java 实现:模拟退火核心流程
/**
* 执行单次模拟退火过程
* @param initialTemp 初始温度
* @param coolingRate 温度衰减系数 (0 < alpha < 1)
* @param minTemp 终止温度阈值
* @return 若找到零能量解则返回 true
*/
public boolean anneal(double initialTemp, double coolingRate, double minTemp) {
double temperature = initialTemp;
while (temperature > minTemp) {
if (currentEnergy == 0) {
return true; // 找到完美解
}
// 记录交换位置以便撤销
int i = random.nextInt(n);
int j = random.nextInt(n);
while (i == j) {
j = random.nextInt(n);
}
int savedI = board[i];
int savedJ = board[j];
int oldEnergy = currentEnergy;
// 增量计算能量变化
int oldContrib = contribution(i) + contribution(j);
board[i] = savedJ;
board[j] = savedI;
int newContrib = contribution(i) + contribution(j);
int delta = newContrib - oldContrib;
currentEnergy = oldEnergy + delta;
// Metropolis 准则:决定是否接受新状态
if (delta > 0) {
// 能量变差,以概率 exp(-delta/T) 接受
double acceptanceProb = Math.exp(-delta / temperature);
if (random.nextDouble() >= acceptanceProb) {
// 拒绝:恢复状态
board[i] = savedI;
board[j] = savedJ;
currentEnergy = oldEnergy;
}
}
// 若 delta <= 0,新状态自动被接受(已交换完成)
// 指数降温
temperature *= coolingRate;
}
return currentEnergy == 0;
}
/**
* 带重启策略的求解器
* 若单次退火失败,则重新随机初始化并再次尝试
*/
public boolean solve(int maxRestarts) {
for (int attempt = 1; attempt <= maxRestarts; attempt++) {
randomInit();
boolean success = anneal(10.0, 0.999, 1e-6);
if (success) {
System.out.println("求解成功!共尝试 " + attempt + " 次退火过程");
return true;
}
}
System.out.println("求解失败,已达到最大重启次数:" + maxRestarts);
return false;
}
六、完整可运行代码
以下是可以直接编译运行的完整程序,包含棋盘可视化与多组测试:
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.ArrayList;
/**
* 八皇后问题 - 模拟退火求解器(完整可运行版本)
*
* 核心思路:
* 1. 将棋盘状态编码为 0~n-1 的排列,天然消除行冲突和列冲突
* 2. 能量函数定义为对角线冲突的皇后对数
* 3. 通过随机交换两列生成邻域状态
* 4. 按 Metropolis 准则接受或拒绝新状态,温度指数衰减
* 5. 失败后自动重启,保证高成功率
*/
public class NQueensSimulatedAnnealing {
private final int n;
private final int[] board;
private int currentEnergy;
private final java.util.Random random = new java.util.Random();
public NQueensSimulatedAnnealing(int n) {
this.n = n;
this.board = new int[n];
}
/** 随机初始化:生成 0~n-1 的随机排列 */
private void randomInit() {
List<Integer> list = new ArrayList<>();
for (int i = 0; i < n; i++) list.add(i);
Collections.shuffle(list, random);
for (int i = 0; i < n; i++) board[i] = list.get(i);
currentEnergy = computeEnergy();
}
/** 计算对角线冲突数 */
private int computeEnergy() {
int attacks = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
if (Math.abs(board[i] - board[j]) == Math.abs(i - j)) {
attacks++;
}
}
}
return attacks;
}
/** 计算第 col 列与其他列的对角线冲突数 */
private int contribution(int col) {
int count = 0;
for (int k = 0; k < n; k++) {
if (k != col && Math.abs(board[col] - board[k]) == Math.abs(col - k)) {
count++;
}
}
return count;
}
/** 单次模拟退火 */
public boolean anneal(double initialTemp, double coolingRate, double minTemp) {
double temperature = initialTemp;
while (temperature > minTemp) {
if (currentEnergy == 0) return true;
int i = random.nextInt(n);
int j = random.nextInt(n);
while (i == j) j = random.nextInt(n);
int savedI = board[i];
int savedJ = board[j];
int oldEnergy = currentEnergy;
int oldContrib = contribution(i) + contribution(j);
board[i] = savedJ;
board[j] = savedI;
int newContrib = contribution(i) + contribution(j);
int delta = newContrib - oldContrib;
currentEnergy = oldEnergy + delta;
if (delta > 0) {
double prob = Math.exp(-delta / temperature);
if (random.nextDouble() >= prob) {
board[i] = savedI;
board[j] = savedJ;
currentEnergy = oldEnergy;
}
}
temperature *= coolingRate;
}
return currentEnergy == 0;
}
/** 带重启策略的求解 */
public boolean solve(int maxRestarts) {
for (int attempt = 1; attempt <= maxRestarts; attempt++) {
randomInit();
if (anneal(10.0, 0.999, 1e-6)) {
System.out.println("✓ 求解成功!尝试次数:" + attempt);
return true;
}
}
System.out.println("✗ 求解失败,已达最大重启次数");
return false;
}
/** 打印棋盘 */
public void printBoard() {
char[][] grid = new char[n][n];
for (char[] row : grid) Arrays.fill(row, '.');
for (int col = 0; col < n; col++) {
grid[board[col]][col] = 'Q';
}
System.out.println(" " + "-".repeat(n * 2 + 1));
for (int r = 0; r < n; r++) {
System.out.print(" |");
for (int c = 0; c < n; c++) {
System.out.print(grid[r][c] + "|");
}
System.out.println();
}
System.out.println(" " + "-".repeat(n * 2 + 1));
}
/** 获取当前解的一维数组表示 */
public int[] getSolution() {
return board.clone();
}
public static void main(String[] args) {
System.out.println("=== 八皇后问题:模拟退火求解 ===\n");
// 测试不同规模
int[] sizes = {8, 10, 12, 15, 20};
for (int size : sizes) {
System.out.println("--- 棋盘大小:" + size + " ---");
NQueensSimulatedAnnealing solver = new NQueensSimulatedAnnealing(size);
long start = System.currentTimeMillis();
boolean ok = solver.solve(100); // 最多重启 100 次
long elapsed = System.currentTimeMillis() - start;
if (ok) {
System.out.println("解向量(列→行):" + Arrays.toString(solver.getSolution()));
if (size <= 12) {
solver.printBoard();
}
}
System.out.println("耗时:" + elapsed + " ms\n");
}
}
}
运行结果示例
=== 八皇后问题:模拟退火求解 ===
--- 棋盘大小:8 ---
✓ 求解成功!尝试次数:1
解向量(列→行):[4, 2, 0, 6, 1, 7, 5, 3]
-----------------
|.|.|.|.|Q|.|.|.|
|.|.|Q|.|.|.|.|.|
|Q|.|.|.|.|.|.|.|
|.|.|.|.|.|.|Q|.|
|.|Q|.|.|.|.|.|.|
|.|.|.|.|.|.|.|Q|
|.|.|.|.|.|Q|.|.|
|.|.|.|Q|.|.|.|.|
-----------------
耗时:12 ms
--- 棋盘大小:20 ---
✓ 求解成功!尝试次数:2
解向量(列→行):[3, 12, 19, 7, 0, 15, 8, 17, 1, ...]
耗时:45 ms
七、时间复杂度与算法分析
| 指标 | 复杂度 | 说明 |
|---|---|---|
| 单次能量计算 | $O(n^2)$ | 遍历所有皇后对统计对角线冲突 |
| 增量能量更新 | $O(n)$ | 仅计算与交换位置相关的冲突变化 |
| 单次退火迭代 | $O(n)$ | 主要由增量更新主导 |
| 总退火步数 | $O(\log(T_0 / T_{\min}))$ | 由降温系数决定,与 $n$ 无关 |
| 单次退火总复杂度 | $O(n \cdot \log(T_0 / T_{\min}))$ | 约 $O(n \times 10^4)$ 量级 |
| 成功率(8皇后) | > 99% | 配合重启策略后实际接近 100% |
与回溯法的对比
| 特性 | 回溯法 | 模拟退火 |
|---|---|---|
| 完备性 | 一定能找到所有解 | 概率性,可能陷入局部最优 |
| 时间复杂度(最坏) | $O(n!)$ | 多项式级别(与温度 schedule 相关) |
| 空间复杂度 | $O(n)$(递归栈) | $O(n)$ |
| 适用规模 | $n \leq 20$ 尚可 | $n = 1000$ 也可快速找到可行解 |
| 扩展性 | 难以处理带权约束 | 能量函数可灵活扩展 |
模拟退火的最大优势在于可扩展性:当问题加入额外约束(如某些格子禁止放置、不同位置有不同代价)时,只需修改能量函数,而无需重写搜索框架。对于仅需一个可行解而非全部解的场景,模拟退火往往比回溯法更高效。
八、总结
本文用 Java 实现了八皇后问题的模拟退火求解器,展示了如何将组合优化问题转化为能量最小化模型。核心要点包括:
- 排列编码天然消除了行列冲突,将解空间从 $n^{2n}$ 压缩到 $n!$
- 增量能量更新将每步复杂度从 $O(n^2)$ 降至 $O(n)$,显著提升运行效率
- Metropolis 准则赋予算法跳出局部最优的能力,是模拟退火区别于贪婪搜索的关键
- 重启策略以极低成本将成功率提升到生产可用级别
模拟退火是一种通用性极强的元启发式算法,除了八皇后问题,它还广泛应用于旅行商问题(TSP)、电路布局、作业车间调度等 NP-hard 组合优化场景。掌握这一技术,相当于在算法工具箱中增添了一把处理复杂优化问题的利器。