前言

模拟退火是一种十分玄学的算法。

正文

假设我们现在有这样一个问题:我们的手上有一个长度为 n 的序列,让你求出其中的最大值。
很显然我们可以直接从一到 n 跑一遍整个序列,找到最大值;或者麻烦一点,sort 排个序直接输出。但现在我们非常的闲,并且非要用一个看起来很高级的算法来求解这个简单的问题,那我们该怎么办?
答案是可以使用模拟退火
由于我们还没有介绍什么是模拟退火,所以下面来介绍。

一段来自 OI Wiki 的专业介绍:
模拟退火是一种随机化算法。当一个问题的方案数量极大(甚至是无穷的)而且不是一个单峰函数时,我们常使用模拟退火求解。

其实说白了就是随机算法。
字面意思,模拟退火的实现灵感来自于退火,是模拟了金属热处理工艺中使高温金属以适宜速度降温的过程。
先放个图:

simulated-annealing

这张图中可以看到当前答案从一开始的疯狂横跳逐渐稳定直到固定在最后答案上。
简单地说,模拟退火算法在当前答案附近随机到一个新的答案,如果新答案优于当前答案,则更新当前答案;否则则以一定概率接受这个新答案。
聪明的你一定注意到了,一定概率到底是多少呢?
如果我们设新答案与当前答案的差,也就是新答案减去当前答案的值为 ΔE ,且当前温度为T,那么这个“一定概率”就是 e 的 -ΔE/T 次方。
可以猜出来这个数是小于一的,因为如果大于一就不是以一定概率而是绝对会更新答案了。
回到上面这道题,那我们如何使用模拟退火呢?只需要先随便选取序列中的一个值作为当前答案,再在当前答案附件随机一个新答案并判断更新即可。这里的判断很简单,就是判断新答案的数值是否大于当前答案的数值。于是我们就做完了。

代码怎么写?直接上例题!

实现

洛谷P1337 [JSOI2004] 平衡点 / 吊打XXX

#include<bits/stdc++.h>
using namespace std;
double x[1005],y[1005],w[1005],ansx,ansy,ans=1e9,MAX_TIME=0.8; //三个ans表示最终答案;至于MAX_TIME,可以设到临近时限(以秒为单位)。
int n;
double work(double mx,double my){ //退火里这个理不理解都无所谓,因为这是根据题目要求算答案的。学过物理的应该能理解这一段?
	double lsans=0;
	for(int i=1;i<=n;i++) lsans+=sqrt((x[i]-mx)*(x[i]-mx)+(y[i]-my)*(y[i]-my))*w[i];
	if(lsans<ans) ans=lsans,ansx=mx,ansy=my;
	return lsans;
}
int main(){
	srand(time(0)); //千万不要使用相同的随机种子,不然随机出来的数都是一样的。
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%lf%lf%lf",&x[i],&y[i],&w[i]);
		ansx+=x[i],ansy+=y[i];
	}
	ansx/=n,ansy/=n,ans=work(ansx,ansy); //ansx和ansy取平均数作为初始值(其实我觉得取啥都行),根据ansx和ansy算出ans。
	while((double)clock()/CLOCKS_PER_SEC<MAX_TIME){ //巨好用,可以让你的代码跑到临近时限的时候自动结束while。
		double T=3000,_t=1e-7,down=0.998,nowx=ansx,nowy=ansy,nowans=ans;
        //T表示当前温度,_t表示结束温度,down表示降温系数(可以粗略理解为降温速度)。参数需要自己调整。
		while(T>_t){ //若当前温度没有达到结束温度。
			double newx=nowx+T*(double(rand())/RAND_MAX*2-1),newy=nowy+T*(double(rand())/RAND_MAX*2-1); //在当前答案附件随机一个新坐标。
			double newans=work(newx,newy);
			double delta=newans-nowans; //计算新答案和 ΔE。
			if(exp((-delta)/T)*RAND_MAX>rand()) nowx=newx,nowy=newy,nowans=newans;
            //这里两步判断合成了一步,因为如果 ΔE 小于等于0,那么计算出来的结果也一定大于1。
			T*=down; //降温。
		}
		for(int i=1;i<=800;i++){ //在最终答案附件进行微调,i的结束值自己调整。
			double newx=ansx+T*(double(rand())/RAND_MAX*2-1);
			double newy=ansy+T*(double(rand())/RAND_MAX*2-1);
			work(newx,newy);
		}
	}
	printf("%.3lf %.3lf\n",ansx,ansy);
	return 0;
}

配合注释食用效果更佳!

花絮

其实模拟退火我三月份就学了,但是没学会。于是看了 OI Wiki 的解释以后搓出了上面这道题。
调了一亿年的参数之后,我AC了。

屏幕截图 2025-12-14 162535

但实际上,我并没有过样例。

屏幕截图 2025-12-15 172808

样例输出是“0.577 1.000”。不过我们可以把它理解成模拟退火的正常现象,毕竟它比较随机。

2026-1-10 经 \(Justskr\) 大佬的解释,原来是我手太慢了。手太慢导致输入时间就超过了 MAX_TIME,也就是超过了 0.8 秒。所以退火根本就不会跑。

注:我敲代码+修改+参调一共搞了 2.5h 。