关于模拟退火

感谢 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]分金币

原文地址:https://www.cnblogs.com/KnightL/p/14879164.html