模拟退火学习笔记——用物理诠释数学
在我们谈到模拟退火之前,我们应该了解什么是退火:
将金属加热到一定温度,保持足够时间,然后以适宜速度冷却(通常是缓慢冷却,有时是控制冷却)的一种金属热处理工艺。
也就是说在退火过程中,有一个初温\(T\),末温\(t\),而在这个过程中,根据所学的物理知识,温度越高,分子的运动速度越快,于是随着温度由T->t的过程中,分子运动的速度会越来越慢————如果我们用一个函数上的点来表示物体的分子呢?
如果函数上的这个点具有和分子一样的特性,他的运动从原来的大幅度不规则变得频率越来越小,最后趋于稳定,那我们可不可以控制它,使他稳定在函数中的极值呢?
于是便有了模拟退火
模拟退火的实现
因为是模拟,所以说总会有一些物理上的不定量逐渐变得数学化理想化,比如说单位时间内降低温度的变化量在实际中必然不会一成不变,但是我们可以在程序书写过程中将它固定为一个值,于是我们便有了下面的一个大框架式的代码
while(T>t){
………………;
T*=deltaT;
}
接下来我们就需要往省略号所存在的部分填入东西了:
首先我们考虑到分子的随机性和他的变化规律,我们需要 \(rand()\) 来构造新的点,且必须与\(T\)相关,所以说往往对于这个新的点 \(x\) 都会与温度相关且随机化构造(后文可能会推翻这个观点)。
解决了构造点的问题,我们需要考虑,如果转移之后的y值是更接近极值的,我们便会将答案更改为新的y值。
如果新的y值没有原来的y值优呢?
我们肯定不可能一味的否定不去转移(贪心便是这样废了),需要有概率的转移
那么这个概率是多少呢?
$$e^{\dfrac{\vartriangle f}{kT}}$$
\(e\)是自然对数,为常数,在\(cmath\)头文件中,我们可以使用\(exp()\)来求\(e\)的次方值,而由于概率∈[0,1],而对于\(e^{-x}\)且x为整数时,函数值满足概率要求,所以说对于概率与\(e\)的对数必定为负数,而\(k\)为物理学常数,近似值为1,所以说k不用考虑,则我们只需要考虑y值的变化量和目前的温度也就是\(\frac{\vartriangle f}{T}\),同时由于T>0,所以说\(\vartriangle f\)小于零。
如果概率大于一个随机数,那么就要尝试去进行转移。
我们注意到点的确定和概率都需要用到随机数,那么随机的方式是如何的呢?
随机数
首先先引进几个东西:
rand():在[0,32767]取随机数。
RAND_MAX:固定值32767。
2*rand()-RAND_MAX:在[-32767,32767]中取随机数
新构造点
因为定义域可能包括负数,所以说我们选用2*rand()-RAND_MAX来求新的x,由于与T有关,所以需要乘T,由于是关于原来x的变化量,所以说还需要加上x:
xx=x+(2*rand()-RAND_MAX)*T
当然,具体问题具体分析,不是所有题都需要这样子新构造点
判断概率
如果概率大于一个随机数,那么就要尝试去进行转移。 ——上文
显然,随机数是在0与1之间的,于是我们便用rand()/RAND_MAX表示随机数,这种概率的求法比较固定,如果\(deltay\)表示y值得变化量:
if(exp(-deltay/T)>rand()/RAND_MAX)
if(exp(-deltay/T)*RAND_MAX>rand())
默认\(deltay\)大于0,对于题目,具体问题具体分析。
至此,模拟退火的理论部分就已经结束了,我们来看一看例题。
例题:
洛谷Haywire
本题较为模板,很容易便可以发现,我们在每一次退火中,可以对两个点进行交换,如果更优,那么更新答案,如果甚至还概率还没有随机数高,我们将它换回来
#include<stdio.h>
#include<cmath>
#include<algorithm>
#include<cstdio>
#include<time.h>
using namespace std;
int road[13][13];
int n;
int p[13];
int fworkout(){//函数值求解
int return_num=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=3;j++){
return_num+=abs(p[i]-p[road[i][j]]);
}
}
return return_num;
}
int rans=0x7fffffff;
void SA(){//模拟退火
double startt=90000,endt=1e-16,deltat=0.993;
while(startt>endt){
int dx;
int num=rand()%n+1;
dx=rand()%n+1;
while(dx==num){
num=rand()%n+1;
dx=rand()%n+1;
}
swap(p[num],p[dx]);
int new_ans=fworkout();
if(new_ans<=rans){
rans=new_ans;
}
else if(exp((rans-new_ans)/startt)<rand()/RAND_MAX){
swap(p[num],p[dx]);
}
startt*=deltat;
}
return ;
}
int main(){
srand(time(0));//不用管
scanf("%d",&n);
for(int i=1;i<=n;i++){
for(int j=1;j<=3;j++){
scanf("%d",&road[i][j]);
}
p[i]=i;
}
for(int i=1;i<=257;i++)SA();//模拟退火英文简写
printf("%d",rans/2);
return 0;
}
虽然这道题比较简单,但是我们也可以积累一些技巧:
- 我们可以利用概率没有达到随机数的情况简化程序
- 有时候可以进行多次模拟退火
- 新的x值不一定有固定的转移方式,可以与T无关
- 退火的量不一定是一个,可以是两个
洛谷锯木厂选址
这个题的难度主要在于如何用O(1)的方式求解y值,剩下的就很简单:
#include<stdio.h>
#include<algorithm>
#include<cmath>
using namespace std;
int w[20005],d[20005];
long long s,sumw[20005],sumd[20005];
long long rans=1e16;
int n;
const double delta=0.993,lastt=1e-14;
long long f(int a,int b){
return sumw[a]*sumd[a]+sumd[b]*(sumw[b]-sumw[a])+sumd[n+1]*(sumw[n]-sumw[b])-s;
}
void SA(){
double T=3000;
int aa=2,bb=3;
while(T>lastt){
int a = ((int)(aa+(rand()*2-RAND_MAX)*T)%n+n)%n+1;
int b = ((int)(bb+(rand()*2-RAND_MAX)*T)%n+n)%n+1;
while(a>=b){
a = ((int)(aa+(rand()*2-RAND_MAX)*T)%n+n)%n+1;
b = ((int)(bb+(rand()*2-RAND_MAX)*T)%n+n)%n+1;
}
long long past=f(aa,bb);
long long calc=f(a,b);
if(calc<past){
if(calc<rans)rans=calc;
aa=a;
bb=b;
}
else if(exp(-(calc-past)/T)>rand()/RAND_MAX){
aa=a;
bb=b;
}
T*=delta;
}
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d%d",&w[i],&d[i]);
sumw[i]=sumw[i-1]+w[i];
sumd[i+1]=sumd[i]+d[i];
s+=sumd[i]*w[i];
}
for(int i=1;i<=50;i++){
SA();
}
printf("%lld",rans);
return 0;
}
对比两道题的代码,我们可以发现,初温,退火次数,变化量,末温都会有不同,这也就涉及到了模拟退火中很重要的一点:调参
如果参数大了会TLE,如果参数小了会WA,这也正是这种算法的玄学之处吧……
好了,接下来还为您推荐了这些题目:

浙公网安备 33010602011771号