玄学算法——模拟退火

发布时间 2023-11-13 22:24:23作者: Po7ed

引入

有时我们需要解决一些看似无法解决的问题,比如这题:P1337 [JSOI2004] 平衡点 / 吊打XXX - 洛谷

总不能把每个坐标都枚举过去吧。(当然这道题也有许多其他优秀的算法。)

这时就需要玄学登场了。

模拟退火

什么是退火?

退火是一种金属热处理工艺,指的是将金属缓慢加热到一定温度,保持足够时间,然后以适宜速度冷却。广义上说,退火是一种对材料的热处理工艺,包括金属材料、非金属材料。而且新材料的退火目的也与传统金属退火存在异同。

——百度百科

模拟退火要做的就是在当前解的基础 上按当前温度的幅度随机情况 并判断此解的优劣。形式化地说:

有参数 \(T_0\) 表示初始温度,\(T_k\) 表示终止温度,\(d\) 表示降温系数。\(S\) 表示当前最优解,\(T\) 表示当前温度。注意 \(T_0>T_k\)(即整个退火是降温操作)。

每次通过随机化生成一个解 \(S'\),那么接受这个解的概率为:

\[\begin{cases} 1&\text{S' is better than S}\\ e^{-\frac{\Delta}{T}}&\text{otherwise} \end{cases} \]

其中 \(\Delta\) 表示两个解的差。由于 \(e^{-x}<1~(x>0)\) 且递减,所以 \(\Delta\) 越大(解越差),接受概率越小。同时 \(T\) 越大,接受概率越大(更狂、更不理智、更赌)。(模拟退火含金量最高的地方。好像与玻尔兹曼分布有关。其实不用了解太多。)

那么怎么退火?

  • \(T\gets T_0\)
  • 随机化:
    • 随机解并按概率接受解。随机解时应按 \(T\) 的大小控制随机幅度(具体看代码)。
    • \(T\gets T\times d\)
  • 重复上一步(随机化),如果 \(T<T_k\),算法结束。
#include <iostream>
#include <cmath>
#include <ctime>
using namespace std;

typedef double d;

// 分别为:降温系数、开始温度、结束温度、用于卡时的。
const d down=0.997,Beg=1e5,End=1e-15,MAX_TIME=0.8;
// 降温系数一般接近 1(徐徐降温),开始温度一般足够大(看数据范围),结束温度足够小(看答案精度),卡时常数略小于时限(单位 秒)。

const int N=2145;
int n;
d ansx,ansy,answ;

struct node
{
	int x,y,w;
};

node a[N];

d calc(d x,d y)// 计算解的优劣
{
	d r=0,dx,dy;
	for(int i=1;i<=n;i++)
	{
		dx=x-a[i].x;
		dy=y-a[i].y;
		r+=sqrt(dx*dx+dy*dy)*a[i].w;
	}
	return r;
}

void SA()
{
	d t=Beg;
	while(t>End)
	{
		d ex=ansx+(rand()*2-RAND_MAX)*t,ey=ansy+(rand()*2-RAND_MAX)*t;// 按当前温度随机化解
		// printf("%.5lf %.5lf\n",ex,ey);
		d ew=calc(ex,ey);// 计算优劣
		d de=ew-answ;// 计算优劣差异
		if(de<0)// 随机的解更好
		{
			ansx=ex,ansy=ey,answ=ew;// 接受
		}
		else if(exp(-de/t)*RAND_MAX>rand())// 否则以概率接受
		{
			ansx=ex,ansy=ey;
		}
		t*=down;// 降温
	}
}

inline void init()
{
	srand(time(NULL));
	srand(rand()+114514);
	srand(rand()+1919810);
	srand(rand());
}

int main()
{
	init();
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d %d %d",&a[i].x,&a[i].y,&a[i].w);
		ansx+=a[i].x;
		ansy+=a[i].y;
	}
	ansx/=n,ansy/=n;// 以平均数为初始解
	answ=calc(ansx,ansy);
	while((double)clock()/CLOCKS_PER_SEC<MAX_TIME)SA();// 卡时,有时间就一直模拟退火
	printf("%.3lf %.3lf",ansx,ansy);// 输出解
	return 0;
}

卡时寄巧

定一个略小于时限的卡时常数(必须满足:卡时常数 \(+\) 单次模拟退火用时 \(<\) 时限)。一般定 \(0.8\sim0.9\)(时限 \(1~\text s\))。

这样只要还有时间,就一直跑模拟退火(增加找到解的概率)。

后记

初学模拟退火,以上就是我的心得体会。以后有新东西会补上。

EOF