模拟退火学习笔记

今天做NOIP2021,做到T3,用近乎暴力的模拟退火水了36pts,听说模拟退火可以做到满分,特此来学习。

劝退

关于模拟退火的原理

模拟退火算法来源于固体退火原理,是一种基于概率的算法,将固体加温至充分高,再让其徐徐冷却,加温时,固体内部粒子随温升变为无序状,内能增大,而徐徐冷却时粒子渐趋有序,在每个温度都达到平衡态,最后在常温时达到基态,内能减为最小。(来源于百度百科)

只要你可以理解这个定义,我觉得你就可以理解这个算法了(因为我觉得这个定义理解难度比算法实际高多了)

概述

模拟退火主要用来解决一些最值、最优解等问题,因为是随机化算法,所以一定无法保证其正确性。
我们可以将其与爬山算法比较学习。
爬山算法主要应用于单峰(谷)函数找峰(谷),(为什么不用三分???);而模拟退火则可以在存在多峰的情况下,找到最高峰。
关于两者的算法流程,记得不知道在哪里看到过一个形象的比较:爬山算法像一个目光短浅的兔子去爬山,看到一座山峰就爬上去,到达顶峰就停下;而模拟退火像一个喝醉的酒的兔子,跌跌撞撞的爬山,随着时间的推移,酒逐渐醒了,去找到最优解。

算法原理:模拟物理学中退火徐徐降温的过程,对最优解进行限制。

先贴CODE

点击查看代码
int get()
{
	…………………… 
}

void SA()
{
	double T=_____,t0=_______,eps=________;
	while(T>t0)
	{
		int x=rand();
		change(x);
		int res=get();
		if(res比ans优)	ans=res;
		else if(exp((-/+)(res-ans)/T)*RAND_MAX>rand())	ans=res;
		else	返回原态
		T*=t0; 
	} 
} 

int main()
{
	…… 
	……
	ans=get();//赋初值
	while(clock()<0.95*CLOCKS_PER_SEC) 	SA();//尽量多跑几次
	print(ans); 
 } 

再讲具体实现

  1. 需要设定的值:初温T,降温t0,温度最小值eps。(关于模拟退火要调的参数其实主要就是前两个)
  2. 对其进行模拟退火构成模拟
  3. 随机在数据范围中取出一个数,判断是否满足最优解。如果满足,则替换当前最优解,如不满足,则根据平衡概率接受(link(没什么用)

贴一张远古经典老图

image
模拟退火随温度降低不断稳定的过程

例题

[TJOI2010] 分金币(模板?)

link

这道题正解是魅力四射的DP,我根本不会,但是我们可以明显的看出这道题可以用模拟退火去做。因为这道题的数据范围比较小,其题目要求又是求最优解,所以直接套上模板。
先把数据分为两组,然后处理出初始解,接下来随机选点交换跑模拟退火就好了。
代码

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define t0 0.9897

using namespace std;

int n;
int a[50];

int ans;

int get()
{
	int sum=0;
	for(int i=1;i<=(n+1)/2;i++)		sum+=a[i];
	for(int i=(n+1)/2+1;i<=n;i++)	sum-=a[i];
	return abs(sum); 
}

void SA()
{
	int eps=1e-15,T=5000;
	while(T>eps)
	{
		int x=rand()%n+1,y=rand()%n+1;
		swap(a[x],a[y]);
		int sum=get();
		if(sum<ans)	ans=sum;
		else if(exp((ans-sum)/T)*RAND_MAX<rand())	swap(a[x],a[y]);
		T*=t0;
	}
}

signed main()
{
	srand((unsigned)time(0));
	int T;
	cin>>T;
	while(T--)
	{
		ans=LONG_LONG_MAX;
		cin>>n;
		for(int i=1;i<=n;i++)	cin>>a[i];
		for(int i=1;i<=1000;i++)	SA();
		cout<<ans<<endl;
	}
	return 0;
} 

[NOIP2021] 方差

link

这道题是NOIP2021T3,我们可以把这道题分两部分去考虑。

Sub1

直接按题意跑模拟退火,就可以得到40pts(这部分分给的是真多)

Sub2

仔细分析题目,可以发现其操作的本质其实就是交换相邻两个数的差分。观察样例或者暴力求解几组随机数据,可以发现规律(你谷上有证明):当整个差分序列呈现先单调下降再单调递增时,出现最优值。
所以直接先维护出一个先降后升的差分序列,再对差分序列进行模拟退火,随机交换即可。
考试时我推出了结论,但是我脑子一热,去通过维护原数组去改变差分序列,我真是一个***

点击查看代码
#include<bits/stdc++.h>
#define int long long
#define t0 0.935

using namespace std;

const int N=1e5+100;

int n;
int a[N],c[N];
int ans;

int get()
{
	int sum=0,res=0;
	for(int i=1;i<=n;i++)	a[i]=a[i-1]+c[i];
	for(int i=1;i<=n;i++)	sum+=a[i];
	for(int i=1;i<=n;i++)	res+=(a[i]*n-sum)*(a[i]*n-sum);
	return res/n;
}

void SA()
{
	double T=1000,eps=1e-17;
	while(T>eps)
	{
		int x=rand()%(n-1)+2,y=rand()%(n-1)+2;
		swap(c[x],c[y]);
		int res=get();
		if(res<ans)	ans=res;
		else if(exp((ans-res)/T)<double(rand())/RAND_MAX)	swap(c[x],c[y]);
		T*=t0;
	}
}

signed main()
{
// 	freopen("variance.in","r",stdin);
// 	freopen("variance.out","w",stdout);

	srand(time(0));
	
	cin>>n;
	for(int i=1;i<=n;i++)
	{
		cin>>a[i];
		c[i]=a[i]-a[i-1];
	}
	
	sort(c+2,c+n/2+1,[&](int a,int b){
		return a>b;
	});
	sort(c+n/2+1,c+n+1);
	
	ans=get();
	
	while(clock()<CLOCKS_PER_SEC*0.95)	SA();
	
	cout<<ans<<endl;
		
	return 0;
}

哦,关于这道题,我的参数随便调都可以在你谷上A,但是校内OJ怎么就A不了,建议校内OJ改名叫TLE OJ吧。

结语

关于模拟退火的调参,好像主流方法就是先保证找到最优解,再手动二分去找最优参数。
关于模拟退火的卡时,其实用一个clock()记录一下就好了,但是要注意模拟退火的时间,你就写一个While一定要把最后一次模拟退火的时间算进去,不然就在模拟退火内部直接结束程序。
其实模拟退火最主要的应用其实是去解决一些NPC问题,如旅行商问题,费马点问题等。
这OI路还漫漫啊~~~

一些题目

P1337 [JSOI2004] 平衡点 / 吊打XXX:这道题计算求费马点,一个模拟退火的基本应用
P4035 [JSOI2008] 球形空间产生器:一个很基本的应用(虽然我当时是用搞死校园写的

posted @   袍蚤  阅读(53)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示