模拟退火

%你退火,玄学算法。

大体借鉴了物理上的退火过程产生的算法。特点是过不过、能过多少点全部玄学。

典图:

我们可以看到,温度越低,我们得到的解偏移就越小,得到的答案也就越稳定。

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

设当前温度为 \(T\) ,新状态与当前状态的能量差为 \(\Delta E\) ,那么这个概率就是:

  1. \(P=1\) (新状态更优)
  2. \(P=e^{\frac{-\Delta E}T}\) (新状态更劣)

具体的说整个过程:

  1. 设置初温 \(T_0\) (一个大数),降温系数 \(d\) (一个很接近 \(1\) 但是小于 \(1\) 的数),终止温度 \(T_k\) (一个接近 \(0\) 的正数)。
  2. 使温度 \(T=T_0\) ,然后随机在当前状态的基础上计算新状态并进行状态的转移。
  3. 使温度 \(T=d\times T\) ,如果 \(T<T_k\) 则退出。

我们为了保证解比较优,常常使用得到的答案继续重复进行退火,来迭代得到更优的答案。

下面的代码是 P1337 [JSOI2004] 平衡点 / 吊打XXX的。

题意:求 \(n\) 个点的带权类费马点。

#include <algorithm>
#include <iostream>
#include <cstdio>
#include <cmath>
#include <ctime>
#include <random>
using namespace std;
int n,x[1010],y[1010],w[1010];
double ansx,ansy,answ;
double energy(double x0,double y0){//这个计算答案的函数看题怎么搞
    double ret=0;
    for(int i=1;i<=n;i++)ret+=sqrt((x0-x[i])*(x0-x[i])+(y0-y[i])*(y0-y[i]))*w[i];
    return ret;
}
void SA(){//模拟退火过程 基本是板子
    double T=1e9;//初温
    while(T>1e-13){
        double ex=ansx+(rand()*2-RAND_MAX)*T;
        double ey=ansy+(rand()*2-RAND_MAX)*T;
        double ew=energy(ex,ey);//随机调整状态
        double det=ew-answ;//与最优答案比较
        if(det<0){
            ansx=ex;ansy=ey;answ=ew;//接受状态
        }
        else if(exp(-det/T)*RAND_MAX>rand()){//求最小值用> 最大值用<
            ansx=ex;ansy=ey;//一定概率接受状态(不改变最优答案)
        }
        T*=0.996;//降温
    }
}
int main(){
    srand(time(0));
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%d%d%d",&x[i],&y[i],&w[i]);
        ansx+=x[i];ansy+=y[i];
    }
    ansx/=n;ansy/=n;
    answ=energy(ansx,ansy);
    do{
        SA();
    }while(1.0*clock()/CLOCKS_PER_SEC<0.95);//卡时 能跑则跑
    printf("%.3lf %.3lf\n",ansx,ansy);
    return 0;
}

OI-WIKI上的小技巧:

  1. 分段:如果函数的峰很多、值域比较大,可以分开几段,每段跑一遍,然后取最优解。
  2. 卡时:如果没T就一直跑。

然后如果再跑不过去就开始玄学调参吧。比如调大初温、调小末温、调大/小降温系数、换个随机数种子之类的。

posted @ 2022-09-29 21:07  gtm1514  阅读(56)  评论(0编辑  收藏  举报