模拟退火
%你退火,玄学算法。
大体借鉴了物理上的退火过程产生的算法。特点是过不过、能过多少点全部玄学。
典图:
我们可以看到,温度越低,我们得到的解偏移就越小,得到的答案也就越稳定。
用一句话概括整个流程就是:如果新状态的解更优则修改答案,否则以一定概率接受新状态。
设当前温度为 \(T\) ,新状态与当前状态的能量差为 \(\Delta E\) ,那么这个概率就是:
- \(P=1\) (新状态更优)
- \(P=e^{\frac{-\Delta E}T}\) (新状态更劣)
具体的说整个过程:
- 设置初温 \(T_0\) (一个大数),降温系数 \(d\) (一个很接近 \(1\) 但是小于 \(1\) 的数),终止温度 \(T_k\) (一个接近 \(0\) 的正数)。
- 使温度 \(T=T_0\) ,然后随机在当前状态的基础上计算新状态并进行状态的转移。
- 使温度 \(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上的小技巧:
- 分段:如果函数的峰很多、值域比较大,可以分开几段,每段跑一遍,然后取最优解。
- 卡时:如果没T就一直跑。
然后如果再跑不过去就开始玄学调参吧。比如调大初温、调小末温、调大/小降温系数、换个随机数种子之类的。
快踩