引言
八皇后问题是约束满足问题的经典代表:在8×8棋盘上放置8个皇后,使彼此不能互相攻击。传统回溯法保证找到解但效率较低。本文用Java实现爬山算法(Hill Climbing)结合随机重启策略,通过局部搜索快速逼近全局最优,在百万级状态空间中展现出优异的求解效率。
爬山算法原理
爬山算法是一种贪心局部搜索:
1. 状态表示:每行恰好一个皇后,用数组queens[row] = col编码
2. 评估函数:互相攻击的皇后对数(目标值为0)
3. 邻居生成:将某行皇后移动到同列的其他位置
4. 移动选择:选择使攻击对数减少最多的邻居
5. 随机重启:陷入局部最优时,随机生成新初始状态重新搜索
Java实现
import java.util.*;
/**
* 八皇后问题:爬山算法 + 随机重启
*/
public class NQueensHillClimbing {
private final int N;
private final Random random;
private int restartCount = 0;
private int totalSteps = 0;
public NQueensHillClimbing(int n) {
this.N = n;
this.random = new Random();
}
/**
* 主求解方法:带随机重启的随机梯度爬山
* @return 解的状态数组(queens[row] = col)
*/
public int[] solve() {
while (true) {
int[] state = generateRandomState();
int[] solution = hillClimb(state);
if (solution != null) {
System.out.println("总重启次数: " + restartCount);
System.out.println("总移动步数: " + totalSteps);
return solution;
}
restartCount++;
if (restartCount > 10000) {
throw new RuntimeException("重启次数过多,可能无解?");
}
}
}
/**
* 核心爬山过程
* @param initial 初始状态
* @return 若找到解返回状态,陷入局部最优返回null
*/
private int[] hillClimb(int[] initial) {
int[] current = initial.clone();
int currentAttacks = countAttacks(current);
while (currentAttacks > 0) {
int[] bestNeighbor = null;
int bestAttacks = currentAttacks;
List<int[]> equalBest = new ArrayList<>(); // 记录并列最优
// 遍历所有邻居:逐行尝试所有列位置
for (int row = 0; row < N; row++) {
int originalCol = current[row];
for (int col = 0; col < N; col++) {
if (col == originalCol) continue;
current[row] = col;
int attacks = countAttacks(current);
if (attacks < bestAttacks) {
bestAttacks = attacks;
bestNeighbor = current.clone();
equalBest.clear();
equalBest.add(bestNeighbor);
} else if (attacks == bestAttacks && attacks < currentAttacks) {
// 与当前最优并列,且优于当前状态
equalBest.add(current.clone());
}
}
current[row] = originalCol; // 恢复
}
// 无改进邻居 -> 局部最优
if (bestAttacks >= currentAttacks) {
return null;
}
// 随机选择并列最优之一(随机梯度,避免循环)
current = equalBest.get(random.nextInt(equalBest.size()));
currentAttacks = bestAttacks;
totalSteps++;
}
return current; // 攻击对数为0,找到解
}
/**
* 计算当前状态的互相攻击皇后对数
* 使用增量式计算:每对只计一次
*/
public int countAttacks(int[] queens) {
int attacks = 0;
for (int i = 0; i < N; i++) {
for (int j = i + 1; j < N; j++) {
// 同列攻击
if (queens[i] == queens[j]) attacks++;
// 对角线攻击:行差 == 列差
int rowDiff = Math.abs(i - j);
int colDiff = Math.abs(queens[i] - queens[j]);
if (rowDiff == colDiff) attacks++;
}
}
return attacks;
}
/**
* 增量式攻击计数优化:仅计算某行移动后的变化量
* 时间复杂度从O(N^2)降到O(N)
*/
public int deltaAttacks(int[] queens, int row, int newCol) {
int oldCol = queens[row];
int delta = 0;
for (int r = 0; r < N; r++) {
if (r == row) continue;
// 旧位置的攻击贡献(减去)
if (queens[r] == oldCol) delta--;
if (Math.abs(r - row) == Math.abs(queens[r] - oldCol)) delta--;
// 新位置的攻击贡献(加上)
if (queens[r] == newCol) delta++;
if (Math.abs(r - row) == Math.abs(queens[r] - newCol)) delta++;
}
return delta;
}
/**
* 使用增量评估的优化版爬山(快10倍以上)
*/
public int[] solveFast() {
while (true) {
int[] state = generateRandomState();
int[] solution = hillClimbFast(state);
if (solution != null) {
System.out.println("[Fast] 总重启次数: " + restartCount);
System.out.println("[Fast] 总移动步数: " + totalSteps);
return solution;
}
restartCount++;
}
}
private int[] hillClimbFast(int[] initial) {
int[] current = initial.clone();
int currentAttacks = countAttacks(current);
while (currentAttacks > 0) {
int bestDelta = 0;
List<int[]> bestMoves = new ArrayList<>();
for (int row = 0; row < N; row++) {
int oldCol = current[row];
for (int col = 0; col < N; col++) {
if (col == oldCol) continue;
int delta = deltaAttacks(current, row, col);
if (delta < bestDelta) {
bestDelta = delta;
bestMoves.clear();
bestMoves.add(new int[]{row, col});
} else if (delta == bestDelta && delta < 0) {
bestMoves.add(new int[]{row, col});
}
}
}
if (bestDelta >= 0) return null; // 局部最优
// 执行随机选择的最好移动
int[] move = bestMoves.get(random.nextInt(bestMoves.size()));
currentAttacks += bestDelta;
current[move[0]] = move[1];
totalSteps++;
}
return current;
}
/**
* 随机生成初始状态(每行一个皇后,列随机)
*/
private int[] generateRandomState() {
int[] state = new int[N];
for (int i = 0; i < N; i++) {
state[i] = random.nextInt(N);
}
return state;
}
/**
* 打印棋盘可视化
*/
public void printBoard(int[] queens) {
for (int r = 0; r < N; r++) {
for (int c = 0; c < N; c++) {
System.out.print(queens[r] == c ? " Q" : " .");
}
System.out.println();
}
}
/**
* 模拟退火变体:以一定概率接受劣解,逃离局部最优
*/
public int[] solveSimulatedAnnealing(double initialTemp, double coolingRate) {
int[] current = generateRandomState();
int currentAttacks = countAttacks(current);
double temp = initialTemp;
while (currentAttacks > 0 && temp > 0.001) {
// 随机选择一个邻居
int row = random.nextInt(N);
int col = random.nextInt(N);
if (col == current[row]) continue;
int delta = deltaAttacks(current, row, col);
int newAttacks = currentAttacks + delta;
// Metropolis准则:以概率接受劣解
if (delta < 0 || Math.random() < Math.exp(-delta / temp)) {
current[row] = col;
currentAttacks = newAttacks;
}
temp *= coolingRate;
}
return currentAttacks == 0 ? current : null;
}
public static void main(String[] args) {
NQueensHillClimbing solver = new NQueensHillClimbing(8);
// 测试1:标准爬山 + 随机重启
System.out.println("=== 标准爬山算法 ===");
long t1 = System.currentTimeMillis();
int[] solution1 = solver.solve();
long t2 = System.currentTimeMillis();
System.out.println("耗时: " + (t2 - t1) + "ms\n");
solver.printBoard(solution1);
// 测试2:增量优化版
NQueensHillClimbing solver2 = new NQueensHillClimbing(8);
System.out.println("\n=== 增量评估优化版 ===");
long t3 = System.currentTimeMillis();
int[] solution2 = solver2.solveFast();
long t4 = System.currentTimeMillis();
System.out.println("耗时: " + (t4 - t3) + "ms\n");
solver2.printBoard(solution2);
// 测试3:大规模问题(100皇后)
System.out.println("\n=== 100皇后测试 ===");
NQueensHillClimbing bigSolver = new NQueensHillClimbing(100);
long t5 = System.currentTimeMillis();
int[] bigSol = bigSolver.solveFast();
long t6 = System.currentTimeMillis();
System.out.println("100皇后求解耗时: " + (t6 - t5) + "ms");
System.out.println("最终攻击对数: " + bigSolver.countAttacks(bigSol));
}
}
算法对比分析
| 算法 | 完备性 | 最优性 | 8皇后平均时间 | 100皇后可行性 |
|---|---|---|---|---|
| 回溯法 | 是 | 是 | ~1ms | 不可行(组合爆炸) |
| 标准爬山 | 否 | 是(若找到) | ~5ms | 可行 |
| 增量爬山 | 否 | 是(若找到) | ~0.5ms | 极快 |
| 模拟退火 | 概率完备 | 近似 | ~10ms | 可行 |
八皇后问题的状态空间约40亿,但爬山算法利用局部结构通常在几次重启内找到解。对于N=100的大规模问题,增量评估版可在毫秒级求解。
关键优化技巧
- 增量评估:不重新计算全部攻击对数,仅计算移动行的变化量,复杂度从O(N²)降至O(N)
- 随机梯度:并列最优时随机选择,避免在平坦区域循环
- Sideways Move:允许有限次数的横向移动(攻击数不变),增加逃离局部最优的概率
总结
本文用Java完整实现了八皇后问题的爬山算法求解框架,包含标准版、增量优化版和模拟退火变体。通过将状态空间限制为每行一皇后的排列(从8^16降到8^8),配合高效的增量攻击计数,算法在保持简洁的同时展现出卓越的可扩展性,可轻松处理百皇后级别的大规模问题。