• 关于模拟退火


    感谢 caq 的友情讲解 orz

    模拟退火是一种靠随机化解决最优解问题的一种方法。

    它通常是设定一个初温、降温系数以及极值,在不断降温的过程中随机找最优解。

    对于每次随机到的一个新的方案,如果它的答案比当前更优,那么我们更新当前答案;

    如果不比当前更优,则我们有一定的几率更新答案。这个几率的计算也有特定公式。

    以下设初温为 (T),降温系数为 (d),当前最优解为 ( ext{ans}),随机到的新答案为 ( ext{new})

    那么 (e^{Large{frac{ ext{ans}- ext{new}}{T imes k}}}) 就是我们在遇到非最优解时的更新概率。其中 (k) 大多情况下为 (1)

    具体流程如图所示:

    假设我们求的是最小值,那么概率接受答案的代码如下:

    if(exp(-del/T)>(double)rand()/RAND_MAX) ... ; //如果满足条件进行更新。
    

    因为是随机化算法,要运行多次才能使答案尽量优。

    为了使答案能更优,我们要尽可能地多去进行随机比较,所以我们可以这么写:

    while((double)clock()/CLOCKS_PER_SEC<0.9) SA();//表示在 0.9s 内不断进行随机比较
    

    随机比较的效果也取决于我们选择的参数大小。

    通常情况下,降温系数是个 (< 1) 但尽量接近于 (1) 的实数,最低极值是个 (>0) 但尽量接近于 (0) 的实数,初温是个比较大的整数。

    随着温度的不断降低,答案的更新频率也越来越低。

    具体降温的模板如下:

    void SA(){
      double T=2021;//初温
      while(T>lim){//lim 为极值,这里表示若温度够则不断降温
        ...//此处搭配具体题目写的计算函数进行随机答案
        del=now-ans;//
        if(del<0) ans=now;//这里求的是最小值,若更优则直接更换
        else if(exp(-del/T)>(double)rand()/RAND_MAX) ... ;
        //若有几率更换则进行相应操作(不能直解赋值更新,因为它不优)
        T*=d;//d 为降温系数,这里表示不断降温
      }
    }
    

    当然了,毕竟是随机化算法,所以有的时候要多交几遍才能 AC 也是很正常的事。

    所以调参数,随 rp 也是模拟退火十分重要的一点(

    这个时候稳住心态才是最重要的(

    当然了,模拟退火本身也不是很难,所以我们不扯别的概念啥的了,直接上几个典型题就懂了。

    []

    UVA10228 A Star not a Tree?

    首先明确,我们要随机的是点,即一个横坐标和一个纵坐标。

    我们就通过不断随机这个点,来暴力计算到每一个顶点的距离,并按上面说的方法更新答案即可。

    当然我们也可以对此进行优化。我们运用人类智慧,可以猜测这个点在多边形中间位置。

    当然我们没法直接找出来,所以可以先把每个点的横纵坐标分别加起来,然后除以 (n),之后在这个点周围不断随机即可。

    然后就做完了。要注意点的横纵坐标是实数,以及 UVA 的输出格式。

    #include<ctime>
    #include<cmath>
    #include<cstdio>
    #include<cstdlib>
    #include<cstring>
    #include<iostream>
    #include<algorithm>
    #define maxn 100010
    #define INF 0x3f3f3f3f
    //#define int long long
    
    using namespace std;
    
    int t,n;
    double nowx,nowy,Now,del;
    double limx,limy,ansx,ansy,ans;
    //ansx,ansy 是最优横纵坐标
    //limx,limy 存可能最优的横纵坐标
    const double lim=1e-10,d=0.996;
    struct node{double x,y;}a[maxn];
    
    int read(){
      int s=0,w=1;char ch=getchar();
      while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
      while(ch>='0'&&ch<='9')s=(s<<1)+(s<<3)+ch-'0',ch=getchar();
      return s*w;
    }
    
    double Dis(double sx,double sy,double tx,double ty){
      return sqrt(pow(sx-tx,2)+pow(sy-ty,2));
    }
    
    double calc(double sx,double sy){
      double res=0.0;
      for(int i=1;i<=n;i++)
        res+=Dis(sx,sy,a[i].x,a[i].y);
      return res;
    }
    
    void SA(){
      double T=2021;
      while(T>lim){
        nowx=limx+((rand()<<1)-RAND_MAX)*T;
        nowy=limy+((rand()<<1)-RAND_MAX)*T;
        //在靠中点的点周围进行随机。
        //nowx,nowy 是当前随机到的点。
        Now=calc(nowx,nowy);del=Now-ans;
        if(del<0)ans=Now,ansx=limx=nowx,ansy=limy=nowy;
        else if(exp(-del/T)>(double)rand()/RAND_MAX)limx=nowx,limy=nowy;
        T*=d;
      }
    }
    
    int main(){
      t=read();
      for(int q=1;q<t;q++){
        n=read();
        for(int i=1;i<=n;i++)
          scanf("%lf%lf",&a[i].x,&a[i].y),
          limx+=a[i].x,limy+=a[i].y;
        limx/=(double)n;limy/=(double)n;
        ansx=limx;ansy=limy;ans=calc(limx,limy);
        SA();SA();SA();SA();SA();SA();SA();SA();
        printf("%.0lf
    
    ",ans);limx=limy=ansx=ansy=ans=0.0;
      }
      n=read();
      for(int i=1;i<=n;i++)
        scanf("%lf%lf",&a[i].x,&a[i].y),
        limx+=a[i].x,limy+=a[i].y;
      limx/=(double)n;limy/=(double)n;
      ansx=limx;ansy=limy;ans=calc(limx,limy);
      SA();SA();SA();SA();SA();SA();SA();SA();
      printf("%.0lf
    ",ans);
      return 0;
    }
    

    []

    [JSOI2016]炸弹攻击1

    和上个题很像,不过要注意几个地方。

    当我们随机到一个点时,我们要钦定它的半径。

    由于边缘的敌人可以被消灭,而边缘的建筑不会受损,所以我们可以一直扩展到边缘与建筑边缘重合为止。

    所以我们当前点的半径就是 到建筑中心的距离减去建筑半径 的最小值。

    由于数据范围很小,所以我们随机完每个点后,暴力枚举每个建筑找这个值即可。、

    注意这题求的是最大值,所以符号与上题不同。剩下的与上面基本相似。

    #include<ctime>
    #include<cmath>
    #include<cstdio>
    #include<cstdlib>
    #include<cstring>
    #include<iostream>
    #include<algorithm>
    #define maxn 100010
    #define INF 0x3f3f3f3f
    //#define int long long
    
    using namespace std;
    
    int n,m;
    double nowx,nowy,now,del;
    const double lim=1e-10,d=0.997;
    double R,limx,limy,ansx,ansy,ans;
    struct enemy{double x,y;}e[maxn];
    struct build{double x,y,r;}a[maxn];
    
    int read() {
      int s=0,w=1;char ch=getchar();
      while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
      while(ch>='0'&&ch<='9')s=(s<<1)+(s<<3)+ch-'0',ch=getchar();
      return s*w;
    }
    
    double Dis1(double sx,double sy,double dx,double dy) {
      return sqrt(pow(sx-dx,2)+pow(sy-dy,2));
    }
    
    double Dis2(double sx,double sy,build a) {
      return sqrt(pow(sx-a.x,2)+pow(sy-a.y,2))-a.r;
    }
    
    int calc(double sx,double sy) {
      int res=0.0;
      double dis=R;
      for(int i=1; i<=n; i++)
        dis=min(dis,Dis2(sx,sy,a[i]));
      for(int i=1; i<=m; i++)
        res+=(Dis1(sx,sy,e[i].x,e[i].y)<=dis);
      return res;
    }
    
    void SA() {
      double T=2020;
      while(T>lim) {
        nowx=limx+((rand()<<1)-RAND_MAX)*T;
        nowy=limy+((rand()<<1)-RAND_MAX)*T;
        now=calc(nowx,nowy);del=now-ans;
        if(del>0) ans=now,ansx=limx=nowx,ansy=limy=nowy;
        else if(exp(-del/T)<(double)rand()/RAND_MAX)limx=nowx,limy=nowy;
        T*=d;
      }
    }
    
    int main() {
      n=read();m=read();
      scanf("%lf",&R);
      for(int i=1; i<=n; i++)
        scanf("%lf%lf%lf",&a[i].x,&a[i].y,&a[i].r);
      for(int i=1; i<=m; i++)
        scanf("%lf%lf",&e[i].x,&e[i].y),
      limx+=e[i].x,limy+=e[i].y;
      limx/=(double)m;ansx=limx;
      limy/=(double)m;ansy=limy;ans=calc(limx,limy);
      while((double)clock()/CLOCKS_PER_SEC<0.40)SA();
      printf("%.0lf
    ",ans);
      return 0;
    }
    

    []

    [HAOI2006]均分数据

    我们肯定是要使每一组的值都尽量相似,才能保证方差更小,标准差才能更小。

    手动枚举每一组分确实麻烦,所以我们考虑换个简单方法。同时要结合它的随机性这一特点来处理。

    “你们想想小学生怎么分啊!!!(” ——caq。

    我们可以贪心地来分,我们每找到一个数,就将它加到当前值最少的组合里,然后计算贡献。

    这样看起来很无厘头,但是我们是随机啊!

    我们随机这个序列的顺序,然后用这个方法来做。通过多遍的随机化,我们有很大地概率可以分到正确的方案。

    至于随机打乱顺序,我们可以用 random_shuffle 来实现。

    注意这题的一个坑点,我们分完后只有 (m) 组,但题目中给出的式子被除数是 (n)

    #include<ctime>
    #include<cmath>
    #include<cstdio>
    #include<cstdlib>
    #include<cstring>
    #include<iostream>
    #include<algorithm>
    #define maxn 25
    #define INF 3e5
    //#define int long long
    
    using namespace std;
    
    int n,m;
    double a[maxn],x[maxn];
    double sum,ans=1000.0,now,del;
    
    int read(){
      int s=0,w=1;char ch=getchar();
      while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
      while(ch>='0'&&ch<='9')s=(s<<1)+(s<<3)+ch-'0',ch=getchar();
      return s*w;
    }
    
    double calc(){
      double res=0.0;
      for(int i=1;i<=m;i++)
        res+=pow(double(x[i])-sum,2);
      res/=(double)m;res=sqrt(res);
      return res;
    }
    
    void SA(){
      for(int i=1;i<=INF;i++){
        random_shuffle(a+1,a+n+1);
        memset(x,0,sizeof x);
        int Minx=INF;
        for(int i=1;i<=n;i++){
          int now=1;
          for(int j=1;j<=m;j++)
            if(x[j]<x[now]) now=j;
            x[now]+=a[i];
        }	
        now=calc();ans=min(ans,now);
      }
    }
    
    int main(){
      n=read();m=read();
      for(int i=1;i<=n;i++)
        scanf("%lf",&a[i]),sum+=(double)a[i];
      sum/=(double)m;SA();
      printf("%.2lf
    ",ans);
      return 0;
    }
    

    至于这题为什么不用模拟降温来实现……因为这题答案如果不优就直接会被遗弃,不需要计算概率,所以直接循环也可以啦。

    []

    [SCOI2008]城堡

    是一道有了思路几分钟出代码的黑题呢。

    首先不管怎样,我们计算贡献需要用到两个点之间的最短距离,所以跑最短路。(nle 50) 可以 Floyd。

    另外我们利用人类智慧发现,城堡越多越优,所以我们最多放 (k) 个那就放 (k) 个。

    计算贡献的时候,我们暴力枚举每个点与其它的城堡点,算出他们的最远距离即可。

    然后我们随机每个城堡放在哪里。

    我们可以钦定前 (k) 个城市有城堡,算出贡献然后比较并撤回操作。之后再随机交换两个无城堡的点,同样钦定前 (k) 个城市有城堡。如此多次。

    通过多次随机,我们大概率可以找出正确的城市。

    注意处理重边。然后就完成了,没啥坑点,也不难写,不知道为啥评黑。

    #include<ctime>
    #include<cmath>
    #include<cstdio>
    #include<cstdlib>
    #include<cstring>
    #include<iostream>
    #include<algorithm>
    #define maxn 1010
    #define INF 0x3f3f3f3f
    //#define int long long
    
    using namespace std;
    
    bool vis[maxn],flag[maxn];
    int n,m,k,r[maxn],limx,limy,pos[maxn];
    int ans,now,del;
    int Dis[maxn][maxn];
    const double d=0.998,lim=1e-10;
    
    int read(){
      int s=0,w=1;char ch=getchar();
      while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
      while(ch>='0'&&ch<='9')s=(s<<1)+(s<<3)+ch-'0',ch=getchar();
      return s*w;
    }
    
    void Floyd(){
      for(int k=1;k<=n;k++)
        for(int i=1;i<=n;i++)
          for(int j=1;j<=n;j++)
            Dis[i][j]=min(Dis[i][j],Dis[i][k]+Dis[k][j]);
    }
    
    int calc(){
      int res=-1;
      for(int i=1;i<=n;i++){
        int noww=INF;
        for(int j=1;j<=n;j++){
          if(!vis[j]) continue;
          noww=min(noww,Dis[i][j]);
        }
        res=max(res,noww);
      }
      return res;
    }
    
    void SA(){
      double T=2020;
      while(T>lim){
        T*=d;int sum=0;
        limx=rand()%n+1,limy=rand()%n+1;
        for(int i=1;i<=n;i++)
          if(!flag[i]&&vis[i])vis[i]=0;
        swap(pos[limx],pos[limy]);
        for(int i=1;i<=n;i++){
          if(vis[pos[i]]) continue;
          vis[pos[i]]=1;sum++;
          if(sum==k) break;
        }
        now=calc();del=now-ans;if(del<0) ans=now;
        else if(exp(-del/T)>(double)rand()/RAND_MAX){T*=d;continue;}
        else swap(pos[limx],pos[limy]);
        T*=d;
      }
    }
    
    int main(){
      memset(Dis,0x3f,sizeof Dis);
      n=read();m=read();k=read();
      for(int i=1;i<=n;i++)r[i]=read()+1,pos[i]=i,Dis[i][i]=0;
      for(int i=1;i<=n;i++)
        Dis[i][r[i]]=Dis[r[i]][i]=min(Dis[i][r[i]],read());
      for(int i=1,x;i<=m;i++)x=read()+1,vis[x]=flag[x]=1;
      Floyd();int sum=0;
      for(int i=1;i<=n;i++){
        if(vis[i]) continue;
        vis[i]=1;sum++;
        if(sum==k) break;
      }
      ans=calc();
      while((double)clock()/CLOCKS_PER_SEC<0.4) SA();
      printf("%d
    ",ans);
      return 0;
    }
    

    []

    Coloring

    真的难题他来了。

    这题我调参 + 调细节调了接近 2h,交了 16 遍/px

    首先我们看看怎么计算一个矩阵的贡献。

    可以发现,如果对每个格子都算四个方向的贡献,那么除了矩阵最外面的一圈之外,其余的都被算了两次。

    所以我们可以对于每个格子,只算右方和下方的贡献。这样可以保证不重不漏。

    然后我们第一次先随便填这个矩阵,之后对其进行随机微调。每次随机两个位置,将他们的颜色交换,同时统计贡献。

    我们算出交换前两个位置四个方向的贡献,再算出交换后的,就可以比较出答案是否变优。

    基本思路就是这样了。具体实现的时候,注意要开两个矩阵,一个存最优答案,一个存与当前随机比较的可能最优的答案。

    另外这题调参很恶心,但是 caq 人很好,给了我他的参数,然后我在此基础上调了调就过了。

    #include<ctime>
    #include<cmath>
    #include<cstdio>
    #include<cstdlib>
    #include<cstring>
    #include<iostream>
    #include<algorithm>
    #define maxn 1010
    #define INF 0x3f3f3f3f
    //#define int long long
    
    using namespace std;
    
    int ans,del;
    int limx,limy,Limx,Limy;
    int n,m,c,p[maxn],val[maxn][maxn];
    int Ans[maxn][maxn];
    int Minx=INF;
    const double d=0.999996,lim=1e-15;
    
    int read(){
      int s=0,w=1;char ch=getchar();
      while(ch<'0'||ch>'9'){if(ch=='-')w=-1;ch=getchar();}
      while(ch>='0'&&ch<='9')s=(s<<1)+(s<<3)+ch-'0',ch=getchar();
      return s*w;
    }
    
    int Changex(int x,int y){
      return (val[x-1][y]!=val[x][y]&&x!=1&&x!=n+1);
    } 
    
    int Changey(int x,int y){
      return (val[x][y-1]!=val[x][y]&&y!=1&&y!=m+1);
    }
    
    int Change(int x,int y){
      return ((val[x-1][y]!=val[x][y]&&x!=1&&x!=n+1)+(val[x][y-1]!=val[x][y]&&y!=1&&y!=m+1));
    }
    
    int change(int sx,int sy,int tx,int ty){
      int res=0;
      res-=Changex(sx+1,sy)+Changex(tx+1,ty)+Changey(sx,sy+1)+Changey(tx,ty+1)+Change(sx,sy)+Change(tx,ty);
      swap(val[limx][limy],val[Limx][Limy]);
      res+=Changex(sx+1,sy)+Changex(tx+1,ty)+Changey(sx,sy+1)+Changey(tx,ty+1)+Change(sx,sy)+Change(tx,ty);
      return res;
    }
    
    void SA(){
      double T=500;
      while(T>lim){
        limx=rand()%n+1;limy=rand()%m+1;
        Limx=rand()%n+1;Limy=rand()%m+1;
        del=change(limx,limy,Limx,Limy);
        if(ans+del<Minx){
          for(int i=1;i<=n;i++)
            for(int j=1;j<=m;j++)
              Ans[i][j]=val[i][j];
          ans=ans+del;Minx=ans;
        }
        else if(exp(-del/T)*RAND_MAX>(double)rand())ans+=del;
        else swap(val[limx][limy],val[Limx][Limy]);
        T*=d;
      }
    }
    
    int main(){
      srand(200030103);
      n=read();m=read();c=read();
      for(int i=1;i<=c;i++)p[i]=read();int now=1;
      for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)
        {if(p[now])p[now]--;else now++,p[now]--;val[i][j]=Ans[i][j]=now;}
      for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)ans+=Change(i,j);
      Minx=ans;while(clock()/CLOCKS_PER_SEC<1.5) SA();
      for(int i=1;i<=n;i++){for(int j=1;j<=m;j++)printf("%d ",Ans[i][j]);puts("");}
      return 0;
    }
    

    []

    写在最后

    感觉这东西适合处理一些看上去不好解决,但数据范围很小的问题。

    其实本质也是暴力,只不过是随机多次而已。最重要的不是退火的过程,而是计算函数的写法。

    不过也不能漫无目的地随机,要善于运用人类智慧(雾),比如上面的钦定选择方案或者点的大致位置等等。

    考场上骗分感觉还是可以的。

    例题

    P2210 Haywire
    SP4587 FENCE3 - Electric Fences
    P3878 [TJOI2010]分金币

  • 相关阅读:
    windows2008R2新增磁盘处于脱机状态及介质写入受保护解决办法
    Oracle查询字段结果加单引号以及其他内容
    系统部署Oracle及cmd命令
    如何允许谷歌浏览器Adobe Flash Player一直运行
    浏览器被hao123篡改怎么办?
    Oracle数据库备份还原
    11_程序中的循环
    10_选择结构
    09_控制台输入
    08_运算符
  • 原文地址:https://www.cnblogs.com/KnightL/p/14879164.html
Copyright © 2020-2023  润新知