……

学习笔记:模拟退火

引言

一谈到模拟退火,大家都知道是玄学算法,但是他是如何\(A\)题的呢?

以下为正文:

\(Part.1\) 从和\(OI\)无关的内容说起

模拟退火,即模拟金属退火这一过程,来实现最优解的寻找。
金属退火,对于我们似乎很遥远了,那我们举个实际点的例子吧。
学过化学的都知道,在蒸发结晶时,我们会在蒸发皿还有部分溶剂时停止加热,用余热蒸干剩余液体。
这就是一个退火过程,它的实质就是降温
这样的好处是,在达到目的的前提下,尽量减少了能源(热量)消耗。
这是一个双赢过程。

\(Part.2\) 模拟退火针对的问题

模拟退火是一种通用的算法,特别是对于最优解问题。
所以所有最优解问题都可以用模拟退火,它是一种实用性较高的算法。
对于每个最优解问题,只要套上模拟退火,即使不能\(AC\),也能得到一个可观的的分数。

\(Part.3\)模拟退火的最简单问题

还记得三分法这个题吗?
我们如果不能保证在\([l,r]\)内只有一个峰值,该如何寻找最值呢?
考虑一个无脑的解法,我们维护一个扫描线,按精度从一边向另一边扫,找到峰值后就贪心的认为是最优解。
不过这样显然是错的
最优解可能要先翻过这座山。
考虑全扫一遍,这样的复杂度爆炸。
那我们以一定的概率向前扫,当然是在保存现有最优解的前提下。
当然,这样还会被卡,如果峰值很远,那就会\(TLE\)
既然已经很玄学了,那就更玄学一点吧:
我们考虑直接随机生成点,进行测试。
似乎正确性提高了不少
偷偷地告诉你,这就是模拟退火的一般过程。

\(Part.4\) 如何退火

就像算法的名字一样,直接模拟就好了。
我们形象的设参数:
\(T\)代表退火的初始温度,\(\mathrm\Delta(0<\mathrm\Delta<1)\)代表降温系数,\(T_0\)代表退火的终止温度,即退到此温度后火就没有用了。

  • 这里的降温系数可以理解为单位时间内温度降低到原来的\(\mathrm\Delta\)倍,用代码实现就是这样的:
T*=delta;

\(Part.5\) 这样的参数有何用

这些参数在随机数生成时起着至关重要的作用,一般来说有两个:
\(1\).生成变量:
我们在退火的过程中要记录一个基准值,即退火的标准。
我们假设要生成一个变量\(X\),那么就要在基准数\(x\)的基础上加上一个随机的值,通常是这样实现的:

X=x+((rand()<<1)-RAND_MAX)*T;

慢慢来说,此处的RAND_MAX是指随机数的值域,即rand()\(\in[0,RAND\_MAX]\)RAND_MAX大概是32768左右,那么前面那一大坨的值域就成了

\[[-RAND\_MAX,RAND\_MAX] \]

\(2\).退火标准的确定(即上文中的\(x\)):
考虑贪心。
如果我们找到一个可以碾压现有最优解的值,我们就贪心的认为:可能有更优解在此解周围,我们就希望以这个点基准进行随机找点,把基准值设成他。
如果并无法碾压暂时性的最优解呢?
那我们不能完全抛弃(如果完全抛弃就沦落成无脑贪心了),只以一个概率接受此基准,但并不改变最优解(当然要保存最优解了)。
不知是哪位大神提出了这个概率的最佳值:

\[e^{\dfrac{\mathrm\Delta X}{T}} \]

(公式好丑啊......
这里的\(\mathrm\Delta X\)指现在解与现有最优解的差值,我们这里保证\(\mathrm\Delta 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,\mathrm\Delta,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{\mathrm\Delta X}{T}\)越小(注意分子是负值),我们接受的概率就越小,还是贪心,温度越低,越接近最优解。
这就是它的神奇之处。

\(Part.8\) 时间复杂度

我们首先考虑一次\(SA\)的复杂度。
显然有方程:

\[T×\mathrm\Delta^x=T_0 \]

易得:

\[x=\log_{\mathrm\Delta}\dfrac{T_0}{T} \]

我们设验证解的时间复杂度是\(\mathcal T(n)\),那么一次模拟退火的时间复杂度是:

\[\mathcal O(\mathcal T(n)\log_{\mathrm\Delta}\dfrac{T_0}{T}) \]

如果像上面那样卡时限:

while((double)clock()/CLOCKS_PER_SEC<0.9)SA();

时间复杂度就是\(\mathcal O(\text{能过})\)好了。

\(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\n",ansx);
    return 0;
}

了解这些之后,你就能切紫题了:
题目链接:P1337 [JSOI2004]平衡点 / 吊打XXX
模拟退火奶一口。
根据一个神奇的能量最小原理-->戳我看百科
我们只需计算系统内的能量合即可,即确定绳结点坐标\((x_0,y_0)\),最小化:

\[\sum\limits_{i=1}^n\sqrt{(x_i-x_0)^2+(y_i-y_0)^2}×m_i \]

分析:
随机化坐标,模拟退火即可:

#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\n",ansx,ansy);
    return 0;
}

你以为没了?
确实没了
只是博主仅仅写了这两道题而已,我们还是那样说:
模拟退火是一种通用的算法,特别是对于最优解问题。

\(Part.10\) 模拟退火的短处

虽然模拟退火能解决大多数最优解问题,不过在一些情况下,不能指望用模拟退火\(AC\)
\(1\).检查一次的时间复杂度过大。
这时无法多次退火以达到最优解,直接自闭。
\(2\).函数模型存在数论函数
数论函数是散点形的函数,我们就不能贪心的认为更优解存在于最优解旁。
这样可以拿一些分,不过\(AC\)希望渺茫。

\(Part.11\) 参考资料:

M_sea:浅谈玄学算法——模拟退火
我们神奇的大脑。

终于讲完辣!(给个赞再走呗,客官n(≧▽≦)n)。

posted @ 2020-04-07 21:23  童话镇里的星河  阅读(563)  评论(2编辑  收藏  举报