模拟退火算法
模拟退火算法是一种随机算法,也是考验 RP 的算法。
像我这种非酋,每道题至少交一页多。
他一般适用于求解最优性问题
如下面的这种情况
正文
模拟退火的出发点是基于物理中固体物质的退火过程与一般组合优化问题之间的相似性。模拟退火算法是一种通用的优化算法,
其物理退火过程由加温过程、等温过程、冷却过程这三部分组成。
原理:
模拟退火的原理也和金属退火的原理近似:将热力学的理论套用到统计学上,将搜寻空间内每一点想像成空气内的分子;
分子的能量,就是它本身的动能;而搜寻空间内的每一点,也像空气分子一样带有“能量”,以表示该点对命题的合适程度。
演算法先以搜寻空间内一个任意点作起始:每一步先选择一个“邻居”,然后再计算从现有位置到达“邻居”的概率。
简单的说,模拟退火就是在一种一定范围内求多峰函数最值的算法。它在模拟温度降低的同时得出新解,温度越高,解的变化量越大
随着温度的逐渐降低,解的变化量也渐渐变小,并越发集中在最优解附近。最后温度达到了我们设置的最低温,对应到物理学上
也就是结晶了,这时,我们可以认为当前我们取得的解就是最优解,当然也可能不是,整个算法也就终止了。
一个很形象的图
求解过程
首先,我们 介绍几个参数
\(temp\) 为初始温度, \(d\) 为变化度 \(eps\) 为末温度。
从 \(T0\) 开始得到一个新的 \(t1\) 若此时 \(t1\) 比 \(t0\) 优的话,我们的答案就是 \(t1\) 的答案。
反之,我们要有一定概率来接受这个值。
这个算法很玄学,根据物理学的知识 p(dE) = exp(dE / t)
\(dE\) 指温度变化量, \(t\) 是初始温度。
引用大佬的博客中的一句话
"我个人对于这个概率的理解是这样的:对于 ΔE,如果它较大,即我们遇到了一个劣得多的解,那我们接受它的概率就相对较小,因为 −ΔE较小嘛;而如果 ΔE较小,即我们遇到了一个较劣的解,
我们接受它的概率就较大,因为 −ΔE较大。对于 T,随着时间的增加, T变得越来越小,因此我们把 −ΔE除以 T,这样接受的概率就随着温度的降低而越来越小,因为 −ΔE是一个负数嘛。
而对于整个式子,当 T较大的时候,我们会接受大部分的解,当 T较小的时候,我们就只会接受 ΔE较小的解。关于Metropolis准则的具体证明,过于玄学,这里就不给出了。当然你也可以自己试一下。如果
选择接受 E,则把 E1设置为 E,然后降温并寻找下一个解。"
一般来说,我们会在不超过时间限制的情况下,尽可能的多进行几次模拟退火。
因为你退火的次数越多,得到的解越准确。
看到例题吧
模拟退火板子题了。
我们一般这么得到新坐标
double ax = x + ((rand() << 1) - RAND_MAX) * temp;//原来的坐标加上变化量,就是新坐标
double ay = y + ((rand() << 1) - RAND_MAX) * temp;//记住就好了,一般都这么写
退火代码:
void SA()
{
double temp = 3000;//初始温度一般设为 2000 - 3000 的一个数
double x = stx, y = sty;//坐标参数一般为平均值
while(temp > eps)
{
double ax = x + ((rand() << 1) - RAND_MAX) * temp;//得到新坐标
double ay = y + ((rand() << 1) - RAND_MAX) * temp;
double nowans = calc(ax,ay);
double bianhua = nowans - ans;//算变化量
if(bianhua < 0)//如果当前的解要更优就更新一下初始坐标
{
ans = nowans;
x = ax; y = ay;
stx = ax; sty = ay;
}
else if(exp(-bianhua / temp) * RAND_MAX > rand())//选择一定的概率接受这个解
{
x = ax; y = ay;
}
temp *= d;//退火
}
}
总代码:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#define d 0.996
#define eps 1e-12
using namespace std;
int T,n;
double stx,sty,sumx,sumy,ans,ax[110],ay[110];
double calc(double x,double y)//计算当前的答案
{
double res = 0;
for(int i = 1; i <= n; i++)
{
res += sqrt ((x-ax[i]) * (x-ax[i]) + (y-ay[i]) * (y-ay[i]));
}
return res;
}
void SA()
{
double temp = 3000;
double x = stx, y = sty;
while(temp > eps)
{
double ax = x + ((rand() << 1) - RAND_MAX) * temp;
double ay = y + ((rand() << 1) - RAND_MAX) * temp;
double nowans = calc(ax,ay);
double bianhua = nowans - ans;
if(bianhua < 0)
{
ans = nowans;
x = ax; y = ay;
stx = ax; sty = ay;
}
else if(exp(-bianhua / temp) * RAND_MAX > rand())
{
x = ax; y = ay;
}
temp *= d;
}
}
void work()
{
stx = sumx / n;
sty = sumy / n;
SA(); SA(); SA(); SA(); SA();//多跑几遍退火
}
int main()
{
srand(1e9+7);//以一个质数为种子
scanf("%d",&T);
for(int t = 1; t <= T; t++)
{
scanf("%d",&n);
sumx = 0, sumy = 0;
ans = 2147483647;//答案初始值设一个很大的值
for(int i = 1; i <= n; i++)
{
scanf("%lf%lf",&ax[i],&ay[i]);
sumx += ax[i];
sumy += ay[i];
}
work();
printf("%.0lf\n", ans);
if(t != T) printf("\n");//输出格式很烦人
}
return 0;
}
一般的参数设置是按自己喜好来的,当然了你可能要多交几遍。
正常的参数 初温 \(t\) 一般设为 2000 -3000 的一个数, \(d\) 一般为较小于 1 的数,比如 \(0.85-0.997\) 末温的就设一个较大于 \(0\) 得数 比如 1e-12
如果说,还过不去的话,那你就多调一下参数,多跑几遍退火(也有可能是你太非了)