模拟退火详解

模拟退火学习(030920一上午成果)

前言:

emmmm。
你还在考虑dp死活写不出来吗?
你还在担忧贪心算法的正确性吗?
模拟退火,不要9998,只要你有耐心看完我的博客。
学会模拟退火,去骗分吧!

1. 爬山算法

由来:

想想爬山的过程,要是想爬到最高峰的话,我现在在半途看不到前面更多的山的高度,但我知道离我
最近几座山的高度,我肯定会选择去爬这几座里面最高的那个,剩下的不在看了。这就是爬山,永远找的是当前最优。
于是就会有很明显的问题:全局最优非局部最优。但是可以拿来骗分,换句话说,当达到解的方案数很少时,成功概率较大。
但是真正的题怎么可能方案数较少呢?

2.模拟退火:

算法流程:

可以说是对爬山算法的一点改进吧。
因为我直接舍弃局部不优解太过了,于是呢,就有了下面的情况:
当在当前状态下找到更优秀局部转移时,我们还是让他100%转移,但是在不优秀的局部最优解下,也让他有一定概率转移。

初学(我)的问题

大概明白了算法流程,于是问题来到:

  1. 我该怎么写,怎么应用于其他题上?
  2. 时间复杂度该如何保证?
  3. 我为什么要随机,随机什么?

然后我就去万能的luogu上问了下
这是讨论帖:https://www.luogu.com.cn/discuss/690367?page=1&sort=time-d
由于我也是初学者,对于这几个东西,我只能说,必须要看懂别人的做题思路什么的才明白。

用题来进行理解:

BZOJ1844 Run Away(cqbz的oj上有)

注释暂时贴到了代码里面:

#include<bits/stdc++.h>
using namespace std;
struct node{
	double x;
	double y;
}p[1005];
/*
求距离所有点的最小值最大 
*/
double dis(double  x,double y,double xx,double yy){
	return sqrt((x-xx)*(x-xx)+(y-yy)*(y-yy));
}
int n;
double limitx,limity;
double cal(double x0,double y0){
	double ret=1e9;
	for(int i=1;i<=n;i++){
		ret=min(ret,dis(x0,y0,p[i].x,p[i].y));
	}
	return ret;//尝试x0,y0这个坐标下的答案 
}
//以上是对答案的计算部分
const int T0=10000;//初始温度 
const double d=0.98; //降温系数
const double Tl=1e-14;//最终降到的温度 
double ansx,ansy;//最终答案 
double RD(double T){
	return T*(rand()*2-RAND_MAX);
} 
void fire(){
	int times=10;//多做几次退火减少偶然性问题 
	double best=cal(ansx,ansy);//确定自己最初取得的较优解的答案 
	//注意这里best是写在times之外的,用于表示最终答案的最优值 
	while(times--){
		double ans=best;//ans再times循环内,表示是这一次退火的最终答案 
		
		double mayx=ansx;
		double mayy=ansy;
		for(double T=T0;T>=Tl;T*=d){//模拟降温过程 
			double xx=mayx+RD(T);
			double yy=mayy+RD(T);
			//尝试新的答案,新的答案由上一次答案随机得到.
			if(xx>limitx||yy>limity||xx<0||yy<0){
				continue;//不在范围内的可能性直接byebye 
			} 
			double nowans=cal(xx,yy);
			if(best<nowans){//如果答案更好,更新答案组成 (同于对比全局的最优值) 
				best=nowans;
				ansx=xx;
				ansy=yy;//全局的最优秀 
			} 
			if(ans<nowans||exp((ans-nowans)/T)<=1.0*rand()/RAND_MAX){//寻找局部最优,要不然答案优秀直接更新,要不然随机化更新以跳出可能的局部最优解问题 
                ans=nowans;
				mayx=xx;
				mayy=yy;
            }

		}
	}
	
	
} 
//以上是模拟退火 
int main(){
	ios::sync_with_stdio(false);
	int t;
	cin >> t;
	srand(time(NULL));
	while(t--){
		
		
		cin >> limitx >> limity >> n;
		ansx=0;
		ansy=0;
		for(int i=1;i<=n;i++){
			cin >> p[i].x >> p[i].y;
			ansx+=p[i].x;
			ansy+=p[i].y;
		}
		ansx/=n;
		ansy/=n;//刚开始时建立一个初始答案,这里取得平均值。其实就是你感觉什么答案可能有点优秀就可以尝试把它当为初值。 
		
		//基本输入 
		fire();
		printf("The safest point is (%.1lf, %.1lf).\n",ansx,ansy);
		
	}
	
}


回顾上面的问题:

对于Q1
  1. 我认为退火的过程很明显嘛,就是让T一直变小并且变小的幅度逐渐减小噻。
    但是这个T到底有什么用?(代码里面有关rand的都没写注释嘛)
    于是就有了一下几个小问题:

Q1.1: 代码上exp((ans-nowans)/T)<=1.0*rand()/RAND_MAX是什么意思?
ans:这个东西呢,前面如oiwiki内所说是e的(ΔE/T)次方,表示接受局部较差解的概率。
但又不完全是概率,因为这个概率要随机,于是嘛就有了后面的rand这个东西。

Q1.2: 代码上:double xx=mayx+RD(T);double yy=mayy+RD(T);为什么这么写?
ans:其实T的意义还可用于控制当前最优解下最新最优解的范围,随着T的减小,这个范围也就不断减小,最后趋于稳定,得到最终答案。
但是还是要看概率,所以加个times来尽量避免这种情况。

Q1.3: RD函数的用途
ans:用于控制某一温度下的最优解delta。

对于Q2:

先上结论:时间复杂度在模拟退火中是难以保证的,我们要做到的是:能算就算,尽量多算(以保证答案尽量ok)。
这个时间是和T0,Tl,D,times有关系的。(相信大家也看得出来某个变量变大变小对全局的影响)。
计算的话,大家应该是算得出来的。’
关键在于时间复杂度太高时,这几个变量的取舍。
好的于是又有了一下的分枝问题:
Q2.1:T0的取舍
ans:你的取舍范围肯定是要覆盖到全部值域的,因此你的T0不能特别小,建议开到ans范围限制左右。

Q2.2:times的取舍
ans:大概在5到10次左右就好了,反正在保证Tl和D尽量细的情况下,扒点他们的时间过来。
注:比如说前者用了1e8次计算,你可以稍微让前者粗糙到1e7次计算然后给times10次。
Q2.3:D和lT的取舍(也可以说是一种两者间的博弈)
D想要尽力靠近1,lT希望尽力靠近0。
考虑D的作用:每次让T减小,同时也让接受坏解的概率/选择范围减小。
考虑lT的作用:感觉就是一个下届。
看到上面两者......感觉肯定是优先满足D 再lT呗.
D给个0.98,T0给个10000,LT给个1e-14差不多。
(但还是有概率会G,所以有T组询问的还是尽量别想这种方法吧)

Q2的补充:

你会发现其实每一次我都要用n的时间计算当前随机出来的答案是多少对吧。
这个时间复杂度也要计算哦。
其实呢,我个人觉得,大不了牺牲D到0.95,LT 到1e-6。(这就不是靠运气而是靠寿命了)

对于Q3

我认为在代码中和Q1中,您看的应该差不多清楚了。
这里就(补充)一点:
祝您在考试中遇到模拟退火时rp++。
哦不,最好是++rp。

易错补充:

  1. 关于大于小于等于的事(就是在一定概率下取答案那里,要看好)
  2. 关于RD函数的事:RD都是RD(T)而不是RD(x)或者RD(Y),这我在写练习时G了。
  3. delta必须是正数哦:详情见帖子:https://www.luogu.com.cn/discuss/690695?page=1&sort=time-d
posted @ 2023-09-20 20:36  铃狐sama  阅读(55)  评论(0)    收藏  举报