爬山算法(Hill Climbing)是最经典的局部搜索算法之一,以其简洁的实现和直观的思想广泛应用于函数优化、组合优化、机器学习超参数调优等领域。本文用Java实现多种爬山算法变体,包括最陡上升、随机重启和模拟退火融合策略。
一、算法原理
1.1 基本思想
爬山算法模拟登山过程:从当前解出发,在其邻域中寻找更优的解,如果找到则移动到该解,重复此过程直到无法找到更优解为止。
1.2 算法变体
- 简单爬山(Simple Hill Climbing):选择第一个更优的邻居
- 最陡上升爬山(Steepest-Ascent Hill Climbing):选择所有邻居中最优的一个
- 随机重启爬山(Random-Restart Hill Climbing):多次从不同初始点出发,取最优结果
- 随机爬山(Stochastic Hill Climbing):以概率选择更好的邻居
1.3 局限性
爬山算法容易陷入局部最优。随机重启和模拟退火是常用的改进策略。
二、Java实现
import java.util.*;
import java.util.function.Function;
/**
* 爬山算法系统实现
* 支持多种变体:简单爬山、最陡上升、随机重启
*/
public class HillClimbing {
private final Random random = new Random(42);
/**
* 解的表示接口
*/
public interface Solution {
double evaluate();
List<Solution> getNeighbors();
Solution copy();
}
/**
* N皇后问题的解表示
*/
public static class NQueensSolution implements Solution {
private final int[] queens; // queens[i]表示第i行皇后所在的列
private final int n;
private double cachedScore = Double.NaN;
public NQueensSolution(int n) {
this.n = n;
this.queens = new int[n];
}
public NQueensSolution(int[] queens) {
this.queens = queens.clone();
this.n = queens.length;
}
public static NQueensSolution random(int n, Random rand) {
NQueensSolution sol = new NQueensSolution(n);
for (int i = 0; i < n; i++) {
sol.queens[i] = rand.nextInt(n);
}
return sol;
}
/**
* 评估函数:冲突数越少越好
* 返回负的冲突数(越大越好)
*/
@Override
public double evaluate() {
if (!Double.isNaN(cachedScore)) {
return cachedScore;
}
int conflicts = 0;
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
// 检查是否在同一列或对角线
if (queens[i] == queens[j] ||
Math.abs(queens[i] - queens[j]) == Math.abs(i - j)) {
conflicts++;
}
}
}
cachedScore = -conflicts; // 冲突越少,分数越高
return cachedScore;
}
@Override
public List<Solution> getNeighbors() {
List<Solution> neighbors = new ArrayList<>();
for (int row = 0; row < n; row++) {
for (int col = 0; col < n; col++) {
if (col != queens[row]) {
NQueensSolution neighbor = new NQueensSolution(queens);
neighbor.queens[row] = col;
neighbors.add(neighbor);
}
}
}
return neighbors;
}
@Override
public Solution copy() {
return new NQueensSolution(queens);
}
public boolean isOptimal() {
return evaluate() == 0; // 无冲突
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
sb.append(queens[i] == j ? "Q " : ". ");
}
sb.append("\n");
}
sb.append("冲突数: ").append((int)(-evaluate()));
return sb.toString();
}
}
/**
* 简单爬山算法
* 找到第一个更优邻居就移动
*/
public Solution simpleHillClimbing(Solution initial, int maxIterations) {
Solution current = initial.copy();
double currentScore = current.evaluate();
for (int iter = 0; iter < maxIterations; iter++) {
List<Solution> neighbors = current.getNeighbors();
boolean improved = false;
for (Solution neighbor : neighbors) {
double neighborScore = neighbor.evaluate();
if (neighborScore > currentScore) {
current = neighbor.copy();
currentScore = neighborScore;
improved = true;
break; // 找到第一个更优解就移动
}
}
if (!improved) {
break; // 到达局部最优
}
}
return current;
}
/**
* 最陡上升爬山算法
* 选择所有邻居中最优的一个
*/
public Solution steepestAscentHillClimbing(Solution initial, int maxIterations) {
Solution current = initial.copy();
double currentScore = current.evaluate();
for (int iter = 0; iter < maxIterations; iter++) {
List<Solution> neighbors = current.getNeighbors();
Solution bestNeighbor = null;
double bestScore = currentScore;
for (Solution neighbor : neighbors) {
double neighborScore = neighbor.evaluate();
if (neighborScore > bestScore) {
bestScore = neighborScore;
bestNeighbor = neighbor.copy();
}
}
if (bestNeighbor == null) {
break; // 到达局部最优
}
current = bestNeighbor;
currentScore = bestScore;
}
return current;
}
/**
* 随机重启爬山算法
* 多次从随机初始解出发,返回最优结果
*/
public Solution randomRestartHillClimbing(
Function<Random, Solution> generator,
int restarts,
int maxIterationsPerRun) {
Solution bestOverall = null;
double bestScore = Double.NEGATIVE_INFINITY;
for (int run = 0; run < restarts; run++) {
Solution initial = generator.apply(random);
Solution result = steepestAscentHillClimbing(initial, maxIterationsPerRun);
double score = result.evaluate();
if (score > bestScore) {
bestScore = score;
bestOverall = result.copy();
}
// 如果找到最优解,提前结束
if (bestScore >= 0) {
break;
}
}
return bestOverall;
}
/**
* 随机爬山算法
* 以概率选择更好的邻居,增加探索性
*/
public Solution stochasticHillClimbing(Solution initial, int maxIterations, double temperature) {
Solution current = initial.copy();
double currentScore = current.evaluate();
for (int iter = 0; iter < maxIterations; iter++) {
List<Solution> neighbors = current.getNeighbors();
// 按分数加权随机选择
List<Solution> betterNeighbors = new ArrayList<>();
List<Double> scores = new ArrayList<>();
double totalWeight = 0;
for (Solution neighbor : neighbors) {
double score = neighbor.evaluate();
if (score > currentScore) {
betterNeighbors.add(neighbor.copy());
double weight = Math.exp((score - currentScore) / temperature);
scores.add(weight);
totalWeight += weight;
}
}
if (betterNeighbors.isEmpty()) {
break; // 局部最优
}
// 轮盘赌选择
double r = random.nextDouble() * totalWeight;
double cumulative = 0;
int selected = 0;
for (int i = 0; i < betterNeighbors.size(); i++) {
cumulative += scores.get(i);
if (cumulative >= r) {
selected = i;
break;
}
}
current = betterNeighbors.get(selected);
currentScore = current.evaluate();
}
return current;
}
/**
* 首先选择法(First-Choice Hill Climbing)
* 随机采样邻居,接受第一个更优解
* 适合邻居数量巨大的情况
*/
public Solution firstChoiceHillClimbing(Solution initial, int maxIterations, int samplesPerIter) {
Solution current = initial.copy();
double currentScore = current.evaluate();
for (int iter = 0; iter < maxIterations; iter++) {
boolean improved = false;
for (int s = 0; s < samplesPerIter; s++) {
// 随机采样一个邻居
List<Solution> neighbors = current.getNeighbors();
Solution randomNeighbor = neighbors.get(random.nextInt(neighbors.size()));
double neighborScore = randomNeighbor.evaluate();
if (neighborScore > currentScore) {
current = randomNeighbor.copy();
currentScore = neighborScore;
improved = true;
break;
}
}
if (!improved) {
break;
}
}
return current;
}
/**
* 连续函数优化的爬山算法(以Rosenbrock函数为例)
*/
public static class ContinuousSolution implements Solution {
private final double[] x;
private final Function<double[], Double> objective;
private final double stepSize;
public ContinuousSolution(double[] x, Function<double[], Double> objective, double stepSize) {
this.x = x.clone();
this.objective = objective;
this.stepSize = stepSize;
}
@Override
public double evaluate() {
return objective.apply(x);
}
@Override
public List<Solution> getNeighbors() {
List<Solution> neighbors = new ArrayList<>();
Random rand = new Random();
for (int i = 0; i < x.length; i++) {
// 正负两个方向
for (int sign = -1; sign <= 1; sign += 2) {
double[] newX = x.clone();
newX[i] += sign * stepSize;
neighbors.add(new ContinuousSolution(newX, objective, stepSize));
}
}
// 添加随机扰动邻居
for (int k = 0; k < 5; k++) {
double[] newX = x.clone();
for (int i = 0; i < x.length; i++) {
newX[i] += (rand.nextDouble() - 0.5) * 2 * stepSize;
}
neighbors.add(new ContinuousSolution(newX, objective, stepSize));
}
return neighbors;
}
@Override
public Solution copy() {
return new ContinuousSolution(x, objective, stepSize);
}
public double[] getX() {
return x.clone();
}
}
/**
* Rosenbrock测试函数(香蕉函数)
* 全局最小值在(1,1),f(1,1)=0
*/
public static double rosenbrock(double[] x) {
double a = 1.0, b = 100.0;
double term1 = Math.pow(a - x[0], 2);
double term2 = b * Math.pow(x[1] - x[0] * x[0], 2);
return -(term1 + term2); // 取负转化为最大化问题
}
public static void main(String[] args) {
HillClimbing hc = new HillClimbing();
// === N皇后问题测试 ===
System.out.println("=== N皇后问题 (n=8) ===\n");
int n = 8;
// 简单爬山
System.out.println("--- 简单爬山 ---");
NQueensSolution initial = NQueensSolution.random(n, hc.random);
Solution simpleResult = hc.simpleHillClimbing(initial, 1000);
System.out.println(simpleResult);
// 最陡上升
System.out.println("\n--- 最陡上升爬山 ---");
Solution steepResult = hc.steepestAscentHillClimbing(
NQueensSolution.random(n, hc.random), 1000);
System.out.println(steepResult);
// 随机重启
System.out.println("\n--- 随机重启爬山 (10次重启) ---");
Solution rrResult = hc.randomRestartHillClimbing(
r -> NQueensSolution.random(n, r), 10, 1000);
System.out.println(rrResult);
// 首先选择
System.out.println("\n--- 首先选择爬山 ---");
Solution fcResult = hc.firstChoiceHillClimbing(
NQueensSolution.random(n, hc.random), 1000, 20);
System.out.println(fcResult);
// === 连续函数优化测试 ===
System.out.println("\n\n=== Rosenbrock函数优化 ===");
ContinuousSolution contInitial = new ContinuousSolution(
new double[]{-1.0, -1.0}, HillClimbing::rosenbrock, 0.1);
System.out.println("初始点: (" + contInitial.getX()[0] + ", " + contInitial.getX()[1] + ")");
System.out.println("初始值: " + contInitial.evaluate());
Solution contResult = hc.steepestAscentHillClimbing(contInitial, 1000);
double[] finalX = ((ContinuousSolution) contResult).getX();
System.out.println("最优解: (" + String.format("%.6f", finalX[0]) + ", " +
String.format("%.6f", finalX[1]) + ")");
System.out.println("最优值: " + String.format("%.6f", contResult.evaluate()));
}
}
三、复杂度分析
| 算法变体 | 时间复杂度 | 空间复杂度 | 特点 |
|---|---|---|---|
| 简单爬山 | O(I × N) | O(1) | 容易陷入局部最优 |
| 最陡上升 | O(I × N) | O(N) | 需要评估所有邻居 |
| 随机重启 | O(R × I × N) | O(1) | R为重启次数,逃脱局部最优 |
| 首先选择 | O(I × S) | O(1) | S为每轮采样数,适合大邻域 |
| 随机爬山 | O(I × N) | O(N) | 增加随机性,平衡探索与开发 |
其中I为最大迭代次数,N为邻居数量,R为重启次数。
四、应用场景
- 组合优化:旅行商问题、调度问题、装箱问题的近似求解
- 函数优化:机器学习损失函数最小化、参数调优
- N皇后问题:经典的约束满足问题
- 电路设计:逻辑最小化、布局优化
- 超参数优化:网格搜索的替代方案
五、总结
本文系统实现了爬山算法的多种变体,并以N皇后问题和Rosenbrock函数为例进行了测试。爬山算法虽然简单,但在实际问题中非常有效,尤其是配合随机重启策略后,可以显著提高找到全局最优的概率。理解局部搜索算法的特性,有助于在工程实践中选择合适的优化策略。