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

    %你退火,听上去一个十分高级的算法,实际上他只是贪心的一种随机化,他的思想很简单,一般是随机几个数,然后用这几个数来算出答案,并更新答案。

    但是,如果只是单纯的随机几个数,那么他的也不会叫做%你退火,所以这个算法也有他自己的奥妙。

    %你退火实际上是爬山算法的一种升级版,爬山算法就是要不断的选择更优解,如果到了一个高峰(低谷),他就会满足于这个局部最优解,但是外面的世界他并不会去探索。

    而模拟退火,就很好地解决了这一个问题,他好像一个喝醉酒的兔子,在群山之间乱窜,走着走着,就慢慢醒来了,窜动的频率也就越来越小,这就是模拟退火。

    模拟退火实际上是仿照金属退火,但是为了方便理解,我们还是用以上兔子醉酒的例子来理解:

    一只兔子喝了很多酒,他的体内有很多酒精也不知道喝了酒为什么会醉,所以就用酒精的量来表示酒醉的程度好了)

    然后这只兔子就开始乱跳,找到了一个下坡就向下滚,陷入了低古,也会借着酒劲往上爬。

    但随着酒精量的减少,兔子会渐渐地回复理智,不想爬山,于是他就只是借着下坡的力量或者渐渐减少的酒劲前进,知道体内没有酒精。

    好了,扯淡结束,但是这和我们\(OI\)有什么关系吗?

    其实模拟退火算法就是以上兔子醉酒的实现,我们先设定一个初始酒精量(我一般设为\(1926\)),然后再设定一个每一次爬山兔子损失的酒精量(由于酒精消耗越来越慢,所以我们用乘法来表示消耗,一般的乘数略小于\(1\)就行),然后兔子的酒精量越来越少,知道一个值我们就认为他体内没有酒精了(我一般设为\(1e-18\)

    然后我们就可以用来操作我们的程序

    例题:
    一道最经典的模拟退火的题目就是P1337 [JSOI2004]平衡点 / 吊打TBR,代码如下:

    #include<bits/stdc++.h>
    using namespace std;
    #define il inline
    #define re register
    il int read()
    {
        re int x=0,k=1;re char c=getchar();
        while(c<'0'||c>'9'){if(c=='-') k=-1;c=getchar();}
        while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
        return x*k;
    }
    #define D double
    #define T 1926//初始酒精量
    #define EPS 1e-18//可以理解为酒精级限量
    #define delta 0.9986//每一次的酒精减少量
    #define maxn 1005
    #define inf 123456789101112.0
    struct node
    {
        int x,y,w;
    }e[maxn];
    int n;
    il D getans(D x,D y)
    {
        D ans=0;
        for(re int i=1;i<=n;++i)
        {
            ans+=(sqrt((e[i].x-x)*(e[i].x-x)+(e[i].y-y)*(e[i].y-y)))*e[i].w;
        }
        return ans;
    }//求出当一个点的坐标是(x,y)的答案,一般按照题意模拟即可
    D xans,yans,ans=inf;
    il void get()
    {
        re D xx=xans,yy=yans,t=T;
        while(t>EPS)//只要现在的酒精量还大于极限酒精量
        {
            re D xt=xx+(rand()*2-RAND_MAX)*t;
            re D yt=yy+(rand()*2-RAND_MAX)*t;
            //随机两个数,但是为什么不是直接rand呢?
            //我们想想兔子是怎么跑的,因为他喝醉了酒,所以它可能向前也可能向后,体现出来就是(rand()*2-RAND_MAX)
            //RAND_MAX是一个常量,表示rand的最大值,rand()*2-RAND_MAX的意思是:rand*2可能比RAND_MAX小也可能大,而且是等概率的,所以我们用这个式子可以很好的表示向前和向后
            //为什么要*t呢?因为兔子体内酒精越少,越不想动,所以它动的频率也就越低,由于t是单调递减,所以我们*t可以模拟出酒精越少越不动的样子
            re D now=getans(xt,yt);
            re D deta=now-ans;
            if(deta<0)//如果新的两个点比答案大,就更新答案,也就是相当于往下滚
            {
                xx=xans=xt;
                yy=yans=yt;
                ans=now;
            }
            else if(RAND_MAX*exp(-deta/t)>rand())//这个柿子据说是科学家推出来的,意思是如果当前的答案不优,兔子也会凭着酒劲乱跑
            {
                xx=xt,yy=yt;
            }
            t*=delta;
        }
    }
    il void SA()
    {
    	//由于是随机,所以我们要尽量的多做几遍来保证正确率
        re int pax=11;
        while(pax--) get();
    }
    signed main()
    {
    #ifndef ONLINE_JUDGE
        freopen("a.in","r",stdin);
    #endif
        srand(19260817);
        n=read();
        for(re int i=1;i<=n;++i)
        {
            e[i].x=read(),e[i].y=read(),e[i].w=read();
        }
        SA();
        printf("%.3lf %.3lf",xans,yans);
        return 0;
    }
    

    如果你有疑问:如果这份代码总是WA怎么办?

    法1:调大T(用处不大)

    法2:调大delta(一般就是调这个)

    法3:调小eps(这个也很有用)

    法4:洗把脸

    例题2:P1429 平面最近点对(加强版)

    这道题目和上一题没有什么本质区别,唯一注意的一点就是我们在随机两个点的时候,我们可以先把所有点按照x排好序,因为最近点对的x肯定不会太远,所以我们再算点的时候可以加一个限制,也就是不让两个点的x差太远。

    代码如下:

    #include<bits/stdc++.h>
    using namespace std;
    #define il inline
    #define re register
    il int read()
    {
        re int x=0,k=1;re char c=getchar();
        while(c<'0'||c>'9'){if(c=='-') k=-1;c=getchar();}
        while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
        return x*k;
    }
    #define inf 1234567890.0
    #define debug printf("Now is Line %d\n",__LINE__)
    #define D double
    #define T 1926
    #define delta 0.99992
    #define eps 1e-18
    #define bd 11//这是一个常量,表示允许两个点的x排名的差值
    struct node
    {
        D x,y;
    }e[200005];
    il D far(int a,int b)
    {
        re D x=e[a].x-e[b].x,y=e[a].y-e[b].y;
        return sqrt(x*x+y*y);
    }//计算两点的距离
    int n,now;
    D ans=inf;
    il int p(int x)
    {
        return (rand()&1)?x:-x;
    }
    il void get()
    {
        re D t=T;
        now=rand()%n+1;
        while(t>eps)
        {
            re int tnow=now+p(rand()%bd);
            if(tnow==now||tnow<1||tnow>n) continue;//越界了就continue
            re D nans=far(tnow,now);
            re D data=nans-ans;
            if(data<0)
            {
                now=tnow;
                ans=nans;
            }
            else if(RAND_MAX*exp(data/t)>rand())
            {
                now=tnow;
            }
            t*=delta;
        }
    }
    il void SA()
    {
        re int pax=12;
        while(pax--) get();
    }
    il bool cmp(node a,node b)
    {
        return a.x<b.x;
    }
    int main()
    {
    #ifndef ONLINE_JUDGE
        freopen("a.in","r",stdin);
    #endif
        srand(19260817);
        n=read();
        for(re int i=1;i<=n;++i)
        {
            e[i].x=read(),e[i].y=read();
        }
        sort(e+1,e+n+1,cmp);
        SA();
        printf("%.4lf",ans);
        return 0;
    }
    

    例题3:P3959 宝藏

    这道题的模拟退火实际上不是很标准,我们只是随机生成一个顺序,用Prim的贪心思想跑一边答案就行。

    代码如下:

    #include<bits/stdc++.h>
    using namespace std;
    #define il inline
    #define re register
    #define debug printf("Now is Line %d\n",__LINE__)
    il int read()
    {
        re int x=0,k=1;re char c=getchar();
        while(c<'0'||c>'9'){if(c=='-') k=-1;c=getchar();}
        while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
        return x*k;
    }
    #define T 1926
    #define delta 0.993
    #define eps 1e-18
    #define inf 1234567890
    #define maxn 20
    int n,m,a[maxn][maxn],dis[maxn],ans=inf,id[maxn];
    il int doit()
    {
        memset(dis,-1,sizeof(dis));
        dis[id[1]]=0;
        re int spend=0;
        for(re int i=2;i<=n;++i)
        {
            re int minn=inf;
            for(re int j=1;j<i;++j)
            {
                if(a[id[j]][id[i]]<inf&&(dis[id[j]]+1)*a[id[j]][id[i]]<minn)
                {
                    dis[id[i]]=dis[id[j]]+1;
                    minn=(dis[id[j]]+1)*a[id[j]][id[i]];
                }//模拟Prim的思想贪心
            }
            if(minn==inf) return minn;//如果这个顺序不合法(也就是前面的点无法到达后面的点)
            spend+=minn;
        }
        return spend;
    }
    il void SA()
    {
        re int t=T;
        while(t>eps)
        {
            re int x=rand()%n+1,y=rand()%n+1;
            swap(id[x],id[y]);
            re int now=doit();
            if(now<ans) ans=now;//这个其实没有完全按照兔子喝醉的思想,为什么呢?我们后面会讲。
            t*=delta;
        }
    }
    signed main()
    {
    #ifndef ONLINE_JUDGE
        freopen("a.in","r",stdin);
    #endif
        n=read(),m=read();
        memset(a,127,sizeof(a));
        for(re int i=1;i<=n;++i) id[i]=i,a[i][i]=0;
        for(re int i=1;i<=m;++i)
        {
            re int u=read(),v=read(),w=read();
            a[u][v]=a[v][u]=min(a[u][v],w);
        }
        re int pax=233;
        while(pax--) SA();
        printf("%d",ans);
        return 0;
    }
    

    为什么说他是不完整的模拟退火呢?因为以下代码也可以过

    #include<bits/stdc++.h>
    using namespace std;
    #define il inline
    #define re register
    #define debug printf("Now is Line %d\n",__LINE__)
    il int read()
    {
        re int x=0,k=1;re char c=getchar();
        while(c<'0'||c>'9'){if(c=='-') k=-1;c=getchar();}
        while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
        return x*k;
    }
    #define T 1926
    #define delta 0.993
    #define eps 1e-18
    #define inf 1234567890
    #define maxn 20
    int n,m,a[maxn][maxn],dis[maxn],ans=inf,id[maxn];
    il int doit()
    {
        memset(dis,-1,sizeof(dis));
        dis[id[1]]=0;
        re int spend=0;
        for(re int i=2;i<=n;++i)
        {
            re int minn=inf;
            for(re int j=1;j<i;++j)
            {
                if(a[id[j]][id[i]]<inf&&(dis[id[j]]+1)*a[id[j]][id[i]]<minn)
                {
                    dis[id[i]]=dis[id[j]]+1;
                    minn=(dis[id[j]]+1)*a[id[j]][id[i]];
                }
            }
            if(minn==inf) return minn;
            spend+=minn;
        }
        return spend;
    }
    il void SA()
    {
        random_shuffle(id+1,id+n+1);
        ans=min(ans,doit());//每一次随机生成一个序列,模拟答案即可
    }
    signed main()
    {
    #ifndef ONLINE_JUDGE
        freopen("a.in","r",stdin);
    #endif
        n=read(),m=read();
        memset(a,127,sizeof(a));
        for(re int i=1;i<=n;++i) id[i]=i,a[i][i]=0;
        for(re int i=1;i<=m;++i)
        {
            re int u=read(),v=read(),w=read();
            a[u][v]=a[v][u]=min(a[u][v],w);
        }
        re int pax=23333;
        while(pax--) SA();
        printf("%d",ans);
        return 0;
    }
    

    其实以上两个代码的本质是一样的,所以其实这个算法只能叫做随机化乱搞

    例题4:P3382 【模板】三分法

    其实这个模板也是可以用模拟退火来做的,只是lr比较小,所以我们把初始酒精量调低一点就行了

    代码如下:

    #include<bits/stdc++.h>
    using namespace std;
    #define file(a) freopen(#a".in","r",stdin),freopen(#a".out","w",stdout)
    #define re register
    #define il inline
    #define debug printf("Now is line %d\n",__LINE__)
    #define D double
    #define maxn 20
    #define inf 123456789.0
    #define T 1
    #define delta 0.997
    #define eps 1e-18
    #define rand_max 1
    int n,da;
    D l,r,ans=-inf,xx,a[maxn],zb;
    il D f(D u)
    {
        re D ans=0;
        for(re int i=0;i<=n;++i)
        {
            ans=ans*u+a[i];
        }
        return ans;
    }//统计答案的时候用了秦九韶公式:
    //ax^n+bx^n-1+……yx+z=z+x(y+x(……+(b+ax)))
    //其实不理解用快速幂就行了
    il int random(int x)
    {
        return rand()%x;
    }
    il void SA()
    {
        re D t=T;
        xx=(l+r)/2.0;
        while(t>eps)
        {
            re D now=xx+(rand()*2-RAND_MAX)*t;
            if(now<l) now=l;
            if(now>r) now=r;
            re D nans=f(now);
            re D data=nans-ans;
            if(data>0)
            {
                xx=zb=now;
                ans=nans;
            }
            else if(RAND_MAX*exp(data/t)>rand())
            {
                xx=now;
            }
            t*=delta;
        }
    }
    signed main()
    {
    #ifndef ONLINE_JUDGE
        freopen("a.in","r",stdin);
    #endif
        srand(19260817);
        scanf("%d%lf%lf",&n,&l,&r);
        for(re int i=0;i<=n;++i)
        {
            scanf("%lf",&a[i]);
        }
        re int pax=20;
        while(pax--)
        {
            SA();
        }
        printf("%.5lf\n",zb);
        return 0;
    }
    
    
  • 相关阅读:
    Scilab5.5.2 在Centos5.11下binary安装(注:不是源码编译安装)
    《DSP using MATLAB》Problem 9.5
    Java 出现警告 不能读取AppletViewer的属性文件
    《DSP using MATLAB》Problem 9.4
    《DSP using MATLAB》Problem 9.2
    《DSP using MATLAB》Problem 8.46
    《DSP using MATLAB》Problem 8.45
    风力摆?这是不是太简单了点
    树莓派:基于物联网做的指纹打卡器
    Python之面向对象(一)
  • 原文地址:https://www.cnblogs.com/bcoier/p/10293054.html
Copyright © 2011-2022 走看看