引言
一谈到模拟退火,大家都知道是玄学算法,但是他是如何(A)题的呢?
以下为正文:
(Part.1) 从和(OI)无关的内容说起
模拟退火,即模拟金属退火这一过程,来实现最优解的寻找。
金属退火,对于我们似乎很遥远了,那我们举个实际点的例子吧。
学过化学的都知道,在蒸发结晶时,我们会在蒸发皿还有部分溶剂时停止加热,用余热蒸干剩余液体。
这就是一个退火过程,它的实质就是降温。
这样的好处是,在达到目的的前提下,尽量减少了能源(热量)消耗。
这是一个双赢过程。
(Part.2) 模拟退火针对的问题
模拟退火是一种通用的算法,特别是对于最优解问题。
所以所有最优解问题都可以用模拟退火,它是一种实用性较高的算法。
对于每个最优解问题,只要套上模拟退火,即使不能(AC),也能得到一个可观的的分数。
(Part.3)模拟退火的最简单问题
还记得三分法这个题吗?
我们如果不能保证在([l,r])内只有一个峰值,该如何寻找最值呢?
考虑一个无脑的解法,我们维护一个扫描线,按精度从一边向另一边扫,找到峰值后就贪心的认为是最优解。
不过这样显然是错的
最优解可能要先翻过这座山。
考虑全扫一遍,这样的复杂度爆炸。
那我们以一定的概率向前扫,当然是在保存现有最优解的前提下。
当然,这样还会被卡,如果峰值很远,那就会(TLE)。
既然已经很玄学了,那就更玄学一点吧:
我们考虑直接随机生成点,进行测试。
似乎正确性提高了不少
偷偷地告诉你,这就是模拟退火的一般过程。
(Part.4) 如何退火
就像算法的名字一样,直接模拟就好了。
我们形象的设参数:
(T)代表退火的初始温度,(mathrmDelta(0<mathrmDelta<1))代表降温系数,(T_0)代表退火的终止温度,即退到此温度后火就没有用了。
- 这里的降温系数可以理解为单位时间内温度降低到原来的(mathrmDelta)倍,用代码实现就是这样的:
T*=delta;
(Part.5) 这样的参数有何用
这些参数在随机数生成时起着至关重要的作用,一般来说有两个:
(1).生成变量:
我们在退火的过程中要记录一个基准值,即退火的标准。
我们假设要生成一个变量(X),那么就要在基准数(x)的基础上加上一个随机的值,通常是这样实现的:
X=x+((rand()<<1)-RAND_MAX)*T;
慢慢来说,此处的RAND_MAX
是指随机数的值域,即rand()
(in[0,RAND\_MAX]),RAND_MAX
大概是32768左右,那么前面那一大坨的值域就成了
(2).退火标准的确定(即上文中的(x)):
考虑贪心。
如果我们找到一个可以碾压现有最优解的值,我们就贪心的认为:可能有更优解在此解周围,我们就希望以这个点基准进行随机找点,把基准值设成他。
如果并无法碾压暂时性的最优解呢?
那我们不能完全抛弃(如果完全抛弃就沦落成无脑贪心了),只以一个概率接受此基准,但并不改变最优解(当然要保存最优解了)。
不知是哪位大神提出了这个概率的最佳值:
(公式好丑啊......
这里的(mathrmDelta X)指现在解与现有最优解的差值,我们这里保证(mathrmDelta X<0),(T)指现在的温度。
用程序实现如下:
if(exp((now-ans)/T)*RAND_MAX>rand()) x=X;
(ps):(exp x)指(e^x)。
考虑一下为什么这样实现。
应该都可以理解的。
(Part.6) 此算法为什么玄学
废话,那么多随机数怎么会不玄学
我们在这里讨论如何使其正确性提高。
(1.)卡时间
我们可以多跑几次模拟退火算法,来提高正确性。
就是这样(时限(1s)):
while((double)clock()/CLOCKS_PER_SEC<0.9)SA();
//模拟退火算法英文简称SA。
//上面那一句将时间的单位由计算机时间单位转化为秒
(2).调参
模拟退火最刺激的就是调参了
一般要调的参数(T,mathrmDelta,T_0,srand())值。
多随机几次,比如:
srand(19260817),srand(rand()),srand(rand());
随机参数好玩
(Part.7) 此算法为什么能(AC)
有rp作保障
这时候我们就要注意退火参数的实际意义了。
观察:
(1).生成实验变量:
就是这一句:
X=x+((rand()<<1)-RAND_MAX)*T;
//注意位运算的优先级,括号不要忘打。
我们发现随着退火,随机量变动变小,准确的说,我们大范围随机了多次,认为越来越接近最优解,效果是这样的:
很形象吧。
(2.)接受概率:
if(exp((now-ans)/T)*RAND_MAX>rand()) x=X;
显然(T)越小,(dfrac{mathrmDelta X}{T})越小(注意分子是负值),我们接受的概率就越小,还是贪心,温度越低,越接近最优解。
这就是它的神奇之处。
(Part.8) 时间复杂度
我们首先考虑一次(SA)的复杂度。
显然有方程:
易得:
我们设验证解的时间复杂度是(mathcal T(n)),那么一次模拟退火的时间复杂度是:
如果像上面那样卡时限:
while((double)clock()/CLOCKS_PER_SEC<0.9)SA();
时间复杂度就是(mathcal O( ext{能过}))好了。
(Part.9) 例题及相关解法
首先,我们尝试用模拟退火算法解决三分法这个题。
链接上面挂上了。
直接上代码了:
#include<cstdio>
#include<iostream>
#include<cstdlib>
#include<cmath>
#include<ctime>
using namespace std;
int n;
double fuc[20],l,r;
double ansx,ans=-1e10;
double f(double g,int n)
{
double x=fuc[n];
for(int i=1;i<=n;i++) x=g*x+fuc[n-i];
return x;
}
void SA()
{
double T=10000,delta=0.993,T_0=1e-14;
double x=ansx;
double X;
while(T>T_0)
{
X=x+((rand()<<1)-RAND_MAX)*T;
X=max(l,X),X=min(r,X);
//我们要将X限制在给定区间内
double now=f(X,n);
if(now>ans) x=X,ansx=x,ans=now;
else if(exp((now-ans)/T)*RAND_MAX>rand()) x=X;
T*=delta;
}
return;
}
void work(){while((double)clock()/CLOCKS_PER_SEC<0.07)SA();}
//注意时限只有100ms
int main()
{
srand(19260817),srand(rand()),srand(rand()),srand(rand());
cin>>n>>l>>r;
for(int i=n;i>=0;i--) cin>>fuc[i];
ansx=(l+r)/2;
//选中间值作为生成基准容易接近正解,这是模拟退火中常见的技巧
work();
printf("%.5lf
",ansx);
return 0;
}
了解这些之后,你就能切紫题了:
题目链接:P1337 [JSOI2004]平衡点 / 吊打XXX
模拟退火奶一口。
根据一个神奇的能量最小原理-->戳我看百科
我们只需计算系统内的能量合即可,即确定绳结点坐标((x_0,y_0)),最小化:
分析:
随机化坐标,模拟退火即可:
#include<cstdio>
#include<iostream>
#include<cstdlib>
#include<cmath>
#include<ctime>
using namespace std;
#define MAXN 1005
double ansx,ansy,ans=1e18;
int n;
double xx[MAXN],yy[MAXN],m[MAXN];
double sumx=0,sumy=0;
double check(double x,double y)
{
double en=0;
for(int i=1;i<=n;i++) en+=sqrt((xx[i]-x)*(xx[i]-x)+(yy[i]-y)*(yy[i]-y))*m[i];
return en;
}
void SA()
{
double T=10000,delta=0.993,T_0=1e-14;
double x=ansx,y=ansy;
double X,Y;
while(T>T_0)
{
X=x+((rand()<<1)-RAND_MAX)*T;
Y=y+((rand()<<1)-RAND_MAX)*T;
double now=check(X,Y);
if(now<ans) x=X,y=Y,ansx=x,ansy=y,ans=now;
else if(exp((ans-now)/T)*RAND_MAX>rand()) x=X,y=Y;
T*=delta;
}
return;
}
void work(){while ((double)clock()/CLOCKS_PER_SEC<0.9) SA();}
int main()
{
srand(19260817),srand(rand()),srand(rand());
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%lf%lf%lf",&xx[i],&yy[i],&m[i]),sumx+=xx[i],sumy+=yy[i];
ansx=sumx/n,ansy=sumy/n;
//仍然找平均处
work();
printf("%.3lf %.3lf
",ansx,ansy);
return 0;
}
你以为没了?
确实没了
只是博主仅仅写了这两道题而已,我们还是那样说:
模拟退火是一种通用的算法,特别是对于最优解问题。
(Part.10) 模拟退火的短处
虽然模拟退火能解决大多数最优解问题,不过在一些情况下,不能指望用模拟退火(AC)。
(1).检查一次的时间复杂度过大。
这时无法多次退火以达到最优解,直接自闭。
(2).函数模型存在数论函数
数论函数是散点形的函数,我们就不能贪心的认为更优解存在于最优解旁。
这样可以拿一些分,不过(AC)希望渺茫。
(Part.11) 参考资料:
M_sea:浅谈玄学算法——模拟退火;
我们神奇的大脑。
终于讲完辣!(给个赞再走呗,客官n(≧▽≦)n)。