模拟退火

模拟退火

简介

模拟退火是一种随机化算法。当一个问题的方案数量极大(甚至是无穷的)而且不是一个单峰函数时,我们常使用模拟退火求解。

实现

根据爬山算法的过程,我们发现:对于一个当前最优解附近的非最优解,爬山算法直接舍去了这个解。而很多情况下,我们需要去接受这个非最优解从而跳出这个局部最优解,即为模拟退火算法。

什么是退火? (选自百度百科)

退火是一种金属热处理工艺,指的是将金属缓慢加热到一定温度,保持足够时间,然后以适宜速度冷却。目的是降低硬度,改善切削加工性;消除残余应力,稳定尺寸,减少变形与裂纹倾向;细化晶粒,调整组织,消除组织缺陷。准确的说,退火是一种对材料的热处理工艺,包括金属材料、非金属材料。而且新材料的退火目的也与传统金属退火存在异同。

由于退火的规律引入了更多随机因素,那么我们得到最优解的概率会大大增加。于是我们可以去模拟这个过程,将目标函数作为能量函数。

模拟退火算法描述

先用一句话概括:如果新状态的解更优则修改答案,否则以一定概率接受新状态。

我们定义当前温度为 T T T,新状态与已知状态(由已知状态通过随机的方式得到)之间的能量(值)差为 Δ E ( Δ E ≥ 0 ) \Delta E(\Delta E\ge 0) ΔE(ΔE0),则发生状态转移(修改最优解)的概率为
P ( Δ E ) = { 1 新状态更优 e − Δ E T 新状态更劣 P(\Delta E)=\begin{cases}1&\text{新状态更优}\\e^\frac{-\Delta E}{T}&\text{新状态更劣}\end{cases} P(ΔE)={1eTΔE新状态更优新状态更劣
注意:我们有时为了使得到的解更有质量,会在模拟退火结束后,以当前温度在得到的解附近多次随机状态,尝试得到更优的解(其过程与模拟退火相似)。

如何退火(降温)?

模拟退火时我们有三个参数:初始温度 T 0 T_0 T0,降温系数 d d d,终止温度 T k T_k Tk。其中 T 0 T_0 T0 是一个比较大的数, d d d 是一个非常接近 1 1 1 但是小于 1 1 1 的数, T k T_k Tk 是一个接近 0 0 0 的正数。

首先让温度 T = T 0 T=T_0 T=T0,然后按照上述步骤进行一次转移尝试,再让 T = d ⋅ T T=d\cdot T T=dT。当 T < T k T<T_k T<Tk 时模拟退火过程结束,当前最优解即为最终的最优解。

注意为了使得解更为精确,我们通常不直接取当前解作为答案,而是在退火过程中维护遇到的所有解的最优值。

引用一张 Wiki - Simulated annealing 的图片(随着温度的降低,跳跃越来越不随机,最优解也越来越稳定)。

img

代码

此处代码以 「BZOJ 3680」吊打 XXX (求 n n n 个点的带权类费马点)为例。

#include<bits/stdc++.h>
using namespace std;
struct data
{
    int x,y,w;
};
struct data a[1005];
double ansx,ansy,answ;
double ans=1e18;//势能与温度
const double down=0.996;
int n;
double power(double x,double y)//总能量越小,越稳定
{
    double sum=0;
    for(int i=1;i<=n;i++)
    {
        double dx=x-a[i].x;
        double dy=y-a[i].y;
        sum+=sqrt(dx*dx+dy*dy)*a[i].w;
    }
    return sum;
}
void sa()
{
    double t=3000;//温度足够高(但温度高,用的时间长)
    while(t>1e-15)//略大于0
    {
        double ex=ansx+(rand()*2-RAND_MAX)*t;//随机产生新的答案,既能产生整数,又能产生负数的随机
        double ey=ansy+(rand()*2-RAND_MAX)*t;
        double ew=power(ex,ey);
        double de=ew-answ;
        if(de<0)//此答案更优秀
        {
            ansx=ex;
            ansy=ey;
            answ=ew;
        }
        else if(exp(-de/t)*RAND_MAX>rand())
        {
            ansx=ex;
            ansy=ey;
        }
        t*=down;
    }
}
void solve()
{
    sa();
    sa();
    sa();
}
int main()
{

    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].w);
        ansx+=a[i].x;
        ansy+=a[i].y;
    }
    ansx/=n;//以平均数作为初始答案
    ansy/=n;
    answ=power(ansx,ansy);
    solve();
    printf("%.3f %.3f\n",ansx,ansy);
}

一些技巧

分块模拟退火

有时函数的峰很多,模拟退火难以跑出最优解。

此时可以把整个值域分成几段,每段跑一遍模拟退火,然后再取最优解。

卡时

有一个 clock() 函数,返回程序运行时间。

可以把主程序中的 simulateAnneal(); 换成 while ((double)clock()/CLOCKS_PER_SEC < MAX_TIME) simulateAnneal(); 。这样子就会一直跑模拟退火,直到用时即将超过时间限制。

这里的 MAX_TIME 是一个自定义的略小于时限的数。

习题

参考链接

OI Wiki

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值