〇、爬山算法(Hill Climbing)
在介绍模拟退火之前,先简单介绍一下爬山算法。
爬山算法(Hill Climbing, HC)是一种简单直接的优化方法。它的核心思想是:从一个初始解出发,不断寻找更好的解。如果找不到更好的解,就停止。对于最小化问题,我们可以把目标函数记为
- 在当前解的邻域中寻找一个更优解。
- 如果找到更优解,就移动到这个解。
- 如果找不到更优解,就停止(说明已经达到局部最优)。
公式表示如下:
爬山算法的一个问题是,它很容易卡在局部最优解上,无法找到全局最优解。比如:
- 局部极小值:算法停在一个“低谷”,但不是全局最低点。
- 平台:周围的解都一样好,算法不知道该往哪走。
- 脊:最优方向和允许的移动方向不一致,导致算法进展缓慢。
多嘴一句:对于连续优化问题,还可以使用梯度下降法(最速下降法):
爬山算法的局限性在于它无法跳出局部最优解,而模拟退火通过引入“温度”参数,可以在一定概率下接受较差的解,从而跳出局部最优。
一、模拟退火
1、模拟退火的背景
模拟退火(Simulated Annealing, SA)来源于物理学中的退火过程。在高温下,原子可以自由移动,容易跨越能量障碍;随着温度降低,系统逐渐稳定到低能量状态。模拟退火将这一过程应用到优化问题中:
- 把目标函数看作“能量”
。 - 把解空间看作“状态”。
- 用“温度”
控制接受较差解的概率。
在物理学中,系统的平衡状态服从 Boltzmann 分布:
模拟退火的核心思想是通过逐步降低温度,让系统从随机状态逐渐收敛到最优解。
2、接受准则
模拟退火的关键是如何决定是否接受一个新解。我们希望构造一个马尔可夫链,使其平稳分布满足:
接受准则可以写成:
这意味着:
- 如果新解更优(
),一定接受。 - 如果新解更差,以一定概率接受,概率为
,其中: 是新解的能量与当前解的能量之差。 是当前温度,控制接受概率的大小。
当温度
3、冷却策略
模拟退火的冷却策略决定了温度如何下降。常见的冷却方法包括:
- 几何降温:
- 指数降温:
- 自适应降温:根据接受率动态调整温度。
理论上,温度下降得足够慢可以保证找到全局最优解,但实际应用中需要在计算开销和精度之间权衡。
4、模拟退火的流程
伪代码:
- 初始化:随机生成初始解
,设定初始温度 。 - 计算当前解的能量
,记录当前最优解。 - 当温度
大于最小值时: - 重复若干次:
- 生成一个新解
。 - 计算能量差
。 - 根据接受准则决定是否接受新解。
- 更新当前最优解。
- 生成一个新解
- 降低温度。
- 重复若干次:
- 输出最优解。
5、参数选择
- 初始温度:让初期接受较差解的概率较高(如 60%-90%)。
- 终止温度:当温度低到无法跳出局部最优时停止。
- 内循环次数:与问题规模成比例。
- 降温速率:通常在 0.85 到 0.95 之间。
二、模拟退火例题
1、题目描述
洛谷 P1337 [JSOI2004] 平衡点 / 吊打XXX
如图,有
每条绳子自上而下穿过桌面上的洞,然后系在一起。图中
注意:桌面上的洞都比绳结
输入格式
文件的第一行为一个正整数
接下来的
输出格式
你的程序必须输出两个浮点数(保留小数点后三位),分别表示处于最终平衡状态时绳结
2、题目分析
关于这道题,一切自然变化进行的方向都是使能量降低,因为能量较低的状态比较稳定。因为物重一定,桌子下面绳子越短,重物越低,势能越小,反过来,桌面上的绳子越长,重物越高,势能越大。
设第
以桌面作为零势能面,桌面以上为正方向,其中
最后,势能的总和即为:
推导结果的前一项
然后就可以使用模拟退火算法来求解这个函数的全局最小值,具体实现方法查看后面的内容。
3、代码拆解
(1)数据读入以及初始选点
scanf("%d", &n);for (int a = 1; a <= n; a++) { scanf("%d%d%d", &object[a].x, &object[a].y, &object[a].w); ansx += object[a].x; ansy += object[a].y;}ansx /= n;ansy /= n;answ = energy(ansx, ansy);
首先读入数据,并将所有洞的坐标取平均作为初始点,当然初始点位可以是任意位置。
(2)能量计算函数
double energy(double x, double y) { double r = 0, dx, dy; for (int a = 1; a <= n; a++) { dx = x - object[a].x; dy = y - object[a].y; r += sqrt(dx * dx + dy * dy) * object[a].w; } return r;}
这个函数计算当前点
(3)模拟退火主循环
void sa() { double t = 3000; while (t > 1e-15) { double nx = ansx + (rand() * 2 - RAND_MAX) * t; double ny = ansy + (rand() * 2 - RAND_MAX) * t; double nw = energy(nx, ny); double de = nw - answ; if (de < 0) { ansx = nx; ansy = ny; answ = nw; } else if (exp(-de / t) * RAND_MAX > rand()) { ansx = nx; ansy = ny; } t *= down; }}
这是模拟退火的核心部分,首先初始设置一个比较高的温度 exp(-de / t) * RAND_MAX > rand()
就是概率的代码表示方法)。最后逐步降低温度。
当然,模拟退火算法是一个看脸的算法,为了提高结果的稳定性,可以多次运行模拟退火算法,取最优结果。
4、完整代码
#include <bits/stdc++.h>
using namespace std;int n;double down = 0.996;struct node { int x; int y; int w;}object[2005];double ansx, ansy, answ;double energy(double x, double y) { double r = 0, dx, dy; for (int a = 1; a <= n; a++) { dx = x - object[a].x; dy = y - object[a].y; r += sqrt(dx * dx + dy * dy) * object[a].w; } return r;}void sa() { double t = 3000; while (t > 1e-15) { double nx = ansx + (rand() * 2 - RAND_MAX) * t; double ny = ansy + (rand() * 2 - RAND_MAX) * t; double nw = energy(nx, ny); double de = nw - answ; if (de < 0) { ansx = nx; ansy = ny; answ = nw; } else if (exp(-de / t) * RAND_MAX > rand()) { ansx = nx; ansy = ny; } t *= down; }}int main() { scanf("%d", &n); for (int a = 1; a <= n; a++) { scanf("%d%d%d", &object[a].x, &object[a].y, &object[a].w); ansx += object[a].x; ansy += object[a].y; } ansx /= n; ansy /= n; answ = energy(ansx, ansy); for (int i = 1; i <= 5; i++) sa(); printf("%.3lf %.3lf\n", ansx, ansy); return 0;}