模拟退火学习笔记
今天做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);
}
再讲具体实现
- 需要设定的值:初温T,降温t0,温度最小值eps。(关于模拟退火要调的参数其实主要就是前两个)
- 对其进行模拟退火构成模拟
- 随机在数据范围中取出一个数,判断是否满足最优解。如果满足,则替换当前最优解,如不满足,则根据平衡概率接受(link(没什么用))
贴一张远古经典老图
模拟退火随温度降低不断稳定的过程
例题
[TJOI2010] 分金币(模板?)
这道题正解是魅力四射的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] 方差
这道题是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] 球形空间产生器:一个很基本的应用(虽然我当时是用搞死校园写的)
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!