【搜索】爬山算法

1. 爬山算法简介

爬山算法是一种启发式搜索算法,用于在解空间中寻找局部最优解(Local Optimum)。它的名字来源于一个形象的比喻:想象你身处一座山中,目标是找到山顶(最高点)。你只能看到周围的地形(局部信息),因此你总是朝着上坡方向(即目标函数值增加的方向)移动,直到到达一个没有更高邻居的点,即“山顶”。

  • 核心思想:从一个初始解出发,在其邻域(附近)中寻找一个比当前解更优的解,如果找到,则移动到该解,并继续搜索;如果找不到,则停止,当前解即为局部最优解。
  • 目标:最大化(或最小化)一个目标函数 f(x)
  • 特点
    • 简单易懂,实现容易。
    • 内存消耗低,通常只维护当前解。
    • 速度快,迭代次数相对较少。
    • 缺点:容易陷入局部最优解,无法保证找到全局最优解(Global Optimum)。它可能会错过更高的山峰(全局最优),而停留在一个较低的山顶(局部最优)。

2. 算法流程

以下是爬山算法的基本步骤:

  1. 初始化

    • 选择一个初始解 current(可以是随机生成的,也可以是根据经验设定的)。
    • 计算初始解的目标函数值 f(current)
  2. 循环迭代

    • 生成邻居:在当前解 current邻域(Neighborhood)内,生成所有可能的邻居解(或采样部分邻居)。
    • 评估邻居:计算每个邻居解的目标函数值。
    • 选择最优邻居
      • 最陡上升爬山法(Steepest-Ascent Hill Climbing):在所有邻居中,选择目标函数值最优(最大或最小)的那个。
      • 首选爬山法(First-Choice Hill Climbing):按某种顺序检查邻居,一旦找到一个比当前解更优的邻居,就立即移动到该邻居(不检查所有邻居)。
    • 更新解
      • 如果找到的最优邻居解 neighbor 比当前解 current 更优(f(neighbor) > f(current) 用于最大化,f(neighbor) < f(current) 用于最小化),则令 current = neighbor,并返回步骤 2 继续迭代。
      • 如果没有邻居比当前解更优,则说明当前解是局部最优解,算法终止
  3. 输出结果:返回当前解 current 作为最终的(局部)最优解。


3. 关键概念

  • 状态空间(State Space):所有可能解的集合。
  • 目标函数(Objective Function) / 适应度函数(Fitness Function):用于评估一个解优劣的函数 f(x)。算法的目标是最大化或最小化这个函数。
  • 邻域(Neighborhood):对于一个给定的解 x,其邻域 N(x) 是指通过一次“小改动”(如翻转一个比特、交换两个元素等)可以从 x 得到的所有解的集合。邻域的定义对算法性能至关重要。
  • 局部最优解(Local Optimum):一个解,其邻域内没有任何解比它更优。
  • 全局最优解(Global Optimum):在整个状态空间中,目标函数值最优的解。
  • 山脊(Ridge):一种地形,沿着某个方向可能没有明显的上升路径。
  • 高原(Plateau):一片平坦区域,所有邻居的目标函数值与当前解相同,算法无法判断移动方向。

4. C++ 实现示例

下面我们以一个经典问题——最大割问题(Max-Cut Problem)——为例来实现爬山算法。

问题描述
给定一个无向图 G = (V, E),将顶点集 V 分成两个不相交的子集 STS ∪ T = V, S ∩ T = ∅),使得连接 ST 两个集合的边(即割边)的总权重最大。

  • 状态表示:一个解可以用一个 bool 数组 assignment 表示,assignment[i] = true 表示顶点 i 属于集合 Sfalse 表示属于集合 T
  • 目标函数:割边的总权重。
  • 邻域定义:通过**翻转(Flip)**一个顶点的分配(从 ST 或从 TS)得到的新解。
#include <iostream>
#include <vector>
#include <cstdlib>
#include <ctime>
#include <algorithm>
#include <random>

using namespace std;

// 图的表示:邻接矩阵,graph[i][j] 表示顶点 i 和 j 之间的边权重
using Graph = vector<vector<int>>;

class HillClimbingMaxCut {
private:
    Graph graph;           // 输入图
    int n;                 // 顶点数量
    vector<bool> current;  // 当前解:顶点分配
    mt19937 rng;           // 随机数生成器

    // 计算当前分配下的割边总权重
    int calculateCutValue(const vector<bool>& assignment) {
        int cutValue = 0;
        for (int i = 0; i < n; ++i) {
            for (int j = i + 1; j < n; ++j) {
                if (graph[i][j] > 0 && assignment[i] != assignment[j]) {
                    cutValue += graph[i][j];
                }
            }
        }
        return cutValue;
    }

    // 执行一次最陡上升爬山步骤
    // 返回:是否找到了更优的邻居
    bool performSteepestAscent() {
        int currentCutValue = calculateCutValue(current);
        int bestImprovement = 0;
        int bestVertexToFlip = -1;

        // 检查翻转每一个顶点带来的变化
        for (int v = 0; v < n; ++v) {
            // 计算翻转顶点 v 后割值的变化量 (Δ)
            int improvement = 0;
            for (int u = 0; u < n; ++u) {
                if (u != v && graph[u][v] > 0) {
                    if (current[u] != current[v]) {
                        // 原本是割边,翻转后不再是割边,损失权重
                        improvement -= graph[u][v];
                    } else {
                        // 原本不是割边,翻转后成为割边,增加权重
                        improvement += graph[u][v];
                    }
                }
            }

            // 寻找能带来最大改进的翻转
            if (improvement > bestImprovement) {
                bestImprovement = improvement;
                bestVertexToFlip = v;
            }
        }

        // 如果找到了能改进的翻转
        if (bestVertexToFlip != -1) {
            current[bestVertexToFlip] = !current[bestVertexToFlip]; // 执行翻转
            return true;
        }

        return false; // 没有找到更优的邻居,已到达局部最优
    }

public:
    // 构造函数
    HillClimbingMaxCut(const Graph& g) : graph(g), n(g.size()), rng(chrono::steady_clock::now().time_since_epoch().count()) {
        current.resize(n);
    }

    // 运行爬山算法
    // 返回:找到的局部最优解及其割值
    pair<vector<bool>, int> solve() {
        // 1. 随机初始化当前解
        for (int i = 0; i < n; ++i) {
            current[i] = rng() % 2; // 随机分配到 S 或 T
        }

        cout << "初始割值: " << calculateCutValue(current) << endl;

        // 2. 迭代直到无法改进
        int iteration = 0;
        while (performSteepestAscent()) {
            iteration++;
            // 可选:打印中间结果
            // cout << "迭代 " << iteration << ", 当前割值: " << calculateCutValue(current) << endl;
        }

        int finalCutValue = calculateCutValue(current);
        cout << "算法结束,迭代次数: " << iteration << ", 最终割值: " << finalCutValue << endl;

        return {current, finalCutValue};
    }

    // 打印最终的划分结果
    void printSolution(const vector<bool>& solution, int cutValue) {
        cout << "最终划分 (S / T):" << endl;
        cout << "S (true): ";
        for (int i = 0; i < n; ++i) {
            if (solution[i]) cout << i << " ";
        }
        cout << endl;

        cout << "T (false): ";
        for (int i = 0; i < n; ++i) {
            if (!solution[i]) cout << i << " ";
        }
        cout << endl;

        cout << "最大割值: " << cutValue << endl;
    }
};

// 主函数
int main() {
    // 创建一个简单的无向图作为测试用例 (5个顶点)
    // 顶点: 0, 1, 2, 3, 4
    // 边: (0,1)=2, (0,2)=3, (1,2)=1, (1,3)=4, (2,4)=2, (3,4)=3
    Graph graph = {
        {0, 2, 3, 0, 0},
        {2, 0, 1, 4, 0},
        {3, 1, 0, 0, 2},
        {0, 4, 0, 0, 3},
        {0, 0, 2, 3, 0}
    };

    cout << "开始运行爬山算法求解最大割问题..." << endl;

    HillClimbingMaxCut hc(graph);
    auto [solution, cutValue] = hc.solve();
    hc.printSolution(solution, cutValue);

    return 0;
}

5. 代码说明

  1. 类设计HillClimbingMaxCut 类封装了算法逻辑。
  2. 随机初始化:使用 std::mt19937std::chrono 生成高质量的随机种子,随机分配每个顶点到 ST
  3. 高效计算calculateCutValue 函数计算总割值。performSteepestAscent 函数在检查每个邻居时,并未完全重新计算割值,而是通过计算翻转一个顶点带来的增量变化(Δ) 来高效判断改进。这是优化的关键。
  4. 最陡上升:算法遍历所有可能的翻转(所有邻居),选择能带来最大正改进(improvement > 0)的那个。如果 bestImprovement <= 0,说明没有更优的邻居。
  5. 终止条件:当 performSteepestAscent 返回 false 时,表示已到达局部最优,循环结束。

6. 算法的局限性与改进方向

  • 局限性

    • 局部最优陷阱:这是最主要的缺点。算法一旦到达一个“山顶”,就无法下山去探索可能更高的“山峰”。
    • 高原和山脊:在高原上,所有邻居的值相同,算法无法移动。在山脊上,可能需要连续的多个移动才能上升,但单步移动可能不优。
    • 对初始解敏感:不同的初始解可能导致收敛到不同的局部最优解。
  • 改进方向(跳出局部最优)

    • 随机重启爬山法(Random-Restart Hill Climbing):多次运行爬山算法,每次使用不同的随机初始解,记录所有运行中找到的最优解。这增加了找到全局最优的概率。
    • 模拟退火(Simulated Annealing):允许以一定概率接受比当前解更差的解,这个概率随着“温度”的降低而减小。这有助于跳出局部最优。
    • 禁忌搜索(Tabu Search):引入一个“禁忌列表”来记住最近访问过的解或移动,防止算法在短时间内循环,鼓励探索新的区域。
    • 遗传算法(Genetic Algorithm):使用种群和进化操作(选择、交叉、变异)来维持解的多样性,避免过早收敛。

总结

爬山算法是一个简单而强大的局部搜索框架。它易于理解、实现和应用。尽管它不能保证找到全局最优解,但在许多实际问题中,它能快速找到质量不错的解。理解其原理、优缺点以及如何定义状态、邻域和目标函数,是应用和改进此类算法的基础。在实际应用中,通常会结合随机重启或其他元启发式策略来克服其局限性。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值