模拟退火

简介

\(\quad\)模拟退火是一种随机化算法。当一个问题的方案数量极大(甚至是无穷的)而且不是一个单峰函数时,我们常使用模拟退火求解。

实现

\(\quad\)很多时候在一个区域内我们需要跳出局部最优解,去接受某些非最优解,才有可能取到全局最优解。

\(\quad\)模拟算法概括:如果新状态的解更优则修改答案,否则以一定概率接受新的状态。

\(\quad\)我们定义当前的温度为 \(T\) ,新状态与已知状态(由已知状态通过随机方式得到)之间的能量差值为 \(\Delta E(\Delta E\ge 0)\),则发生状态转移(修改最优解)的概率为

模拟退火

(exp(-delta/t)*RAND_MAX>rand())

\(\quad\)一定要这么写

注意

\(\quad\)有时我们为了使得到的解更有质量,会在模拟退火结束后,以当前温度在得到的解附近多次随机状态,尝试得到更有的解。

具体实现

\(\quad\)模拟退火一般有三个参数:

  1. 初始温度 \(T_0\)
  2. 降温系数 \(d\)
  3. 终止温度 \(T_k\)

\(\quad\)其中 \(T_0\) 是一个比较大的数, \(d\) 是一个非常接近 1 但是小于 1 的数,\(T_k\) 是一个接近 0 的正数

\(\quad\)首先让温度 \(T=T_0\),然后按照上述步骤尝试转移,再让 \(T=d\times T\),当 \(T<T_k\) 时,模拟退火过程结束,当前最优解即为最终最优解。

注意

\(\quad\)模拟退火本身就是一个玄学的做法,所以你很有可能几次评测都是不同的分值(看你的 rp 了)

\(\quad\)注意为了使得解更为精确,我们通常不直接取当前解作为答案,而是在退火过程中维护遇到的所有解的最优值。

(引用一张 Wiki - Simulated annealing 的图)

模拟退火

一些技巧

分块模拟退火

\(\quad\)有时函数的峰很多,模拟退火难以跑出最优解。

\(\quad\)此时可以把整个值域分成几段,每段跑一遍模拟退火,然后再取最优解。

卡时

用\(\quad\) clock 函数,一直跑模拟退火,直到快要超时,就直接退出去。

例题

Luogu 1337 [JSOI2004]平衡点 / 吊打XXX

\(\quad\)很明显当

\[\sum dis\times w \]

\(\quad\)最小时,肯定更平衡。

\(\quad\)我们随机化坐标。

\(\quad\)一般下一次的选择都可以这么写

		double nxtx=nowx+t*(rand()*2-RAND_MAX);
		double nxty=nowy+t*(rand()*2-RAND_MAX);

code:

#include<bits/stdc++.h>
#define down 0.996
using namespace std;
const int maxn=10005;
int n,x[maxn],y[maxn],w[maxn];
double ansx,ansy,dis;
double calc(double xx,double yy)
{
	double res=0,dx,dy;
	for(int i=1;i<=n;i++)
	{
		dx=x[i]-xx,dy=y[i]-yy;
		res+=sqrt(dx*dx+dy*dy)*w[i];
	}
	return res;
}
void SA()
{
	double t=3000;
	double nowx=ansx,nowy=ansy;
	while(t>1e-15)
	{
		double nxtx=nowx+t*(rand()*2-RAND_MAX);
		double nxty=nowy+t*(rand()*2-RAND_MAX);
		double ew=calc(nxtx,nxty);
		double delta=ew-dis;
		if(delta<0) nowx=nxtx,nowy=nxty,dis=ew,ansx=nxtx,ansy=nxty;
		else if(exp(-delta/t)*RAND_MAX>rand()) nowx=nxtx,nowy=nxty;
		t*=down;
	}
}
int main()
{
	srand(time(0));
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>x[i]>>y[i]>>w[i];
		ansx+=x[i];
		ansy+=y[i];
	}
	ansx/=n;
	ansy/=n;
	dis=calc(ansx,ansy);
	SA();SA();SA();SA();	
	printf("%.3lf %.3lf\n",ansx,ansy);
	return 0;
}

P3878 [TJOI2010]分金币

#include<bits/stdc++.h>
using namespace std;
int T,a[35],best,n;
int calc()
{
	int s1=0,s2=0;
	for(int i=1;i<=(n+1)/2;i++) s1+=a[i];
	for(int i=(n+1)/2+1;i<=n;i++) s2+=a[i];
	return abs(s1-s2);
}
int main()
{
	srand(rand());
	cin>>T;
	while(T--)
	{
		cin>>n;
		for(int i=1;i<=n;i++) cin>>a[i];
		best=0x3f3f3f3f;
		for(int k=1;k<=1000;k++)
		{
			for(register double t=5000;t>1e-10;t*=0.911)
			{
				int x=rand()%n+1;
				int y=rand()%n+1;
				swap(a[x],a[y]);
				int tmp=calc();
				if(tmp<best)	best=tmp;
				else if(exp((best-tmp)/t)*RAND_MAX<(double)rand()) swap(a[x],a[y]);
			}
		}
		cout<<best<<endl;
	}
	return 0;
}

P2503 [HAOI2006]均分数据

#include<bits/stdc++.h>
using namespace std;
int n,m,a[25],s;
double no[25],be[25];
double ave,best,now;
double calc()
{
	double num[10];
	memset(num,0,sizeof(num));
	for(int i=1;i<=n;i++)
	{
		int p=1;
		for(int j=1;j<=m;j++) if(num[j]<num[p]) p=j;
		num[p]+=no[i];
	}
	double tmp=0;
	for(int i=1;i<=m;i++) tmp+=(num[i]-ave)*(num[i]-ave);
	return sqrt((double)tmp/m);
}
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++) cin>>a[i],no[i]=be[i]=a[i],s+=a[i];
	ave=(double)s/m;
	best=calc();
	for(int k=1;k<=30;k++)
	{
		now=best;
		for(int i=1;i<=n;i++) no[i]=be[i];
		for(double t=100000;t>=1e-14;t*=0.996)
		{
			int x=rand()%n+1;
			int y=rand()%n+1;
			while(x==y) y=rand()%n+1;
			swap(no[x],no[y]);
			double tmp=calc(); 
			if(tmp<best)
			{
				best=tmp;
				for(int i=1;i<=n;i++) be[i]=no[i];
			}
		    if(tmp<now || exp((tmp-now)/t)*RAND_MAX>rand()) now=tmp;
			else swap(no[x],no[y]);
		}
	}
	printf("%.2f",best);
	return 0;
}

参考文献(致谢):
OI WIKI

上一篇:Matlab实现斜激波受偏向角、冲击角和马赫数的影响关系


下一篇:分支与循环语句练习——用C语言设计一个猜数字游戏把(随机数的生成)