zoukankan      html  css  js  c++  java
  • 学习笔记:模拟退火

    引言

    一谈到模拟退火,大家都知道是玄学算法,但是他是如何(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左右,那么前面那一大坨的值域就成了

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

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

    [e^{dfrac{mathrmDelta X}{T}} ]

    (公式好丑啊......
    这里的(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)的复杂度。
    显然有方程:

    [T×mathrmDelta^x=T_0 ]

    易得:

    [x=log_{mathrmDelta}dfrac{T_0}{T} ]

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

    [mathcal O(mathcal T(n)log_{mathrmDelta}dfrac{T_0}{T}) ]

    如果像上面那样卡时限:

    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)),最小化:

    [sumlimits_{i=1}^nsqrt{(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
    ",ansx,ansy);
        return 0;
    }
    

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

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

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

    (Part.11) 参考资料:

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

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

  • 相关阅读:
    coredump分析
    Sword LRU算法
    C++ STL迭代器失效问题
    Sword DB主从一致性的解决方法
    Sword CRC算法原理
    C语言 按位异或实现加法
    Linux 等待信号(sigsuspend)
    C语言 宏定义之可变参数
    Linux shell字符串操作
    C++ *和&
  • 原文地址:https://www.cnblogs.com/tlx-blog/p/12656177.html
Copyright © 2011-2022 走看看