模拟退火(附加随机贪心)

idea

主要原理

模拟退火是一种求解多峰函数极值的随机化算法,适用性强,骗分功能强大。

解决多峰函数极值问题首先有一种贪心的方法,考虑每次选择当前值两端的值,如果有更优则将当前值变成更优值,最终到达峰顶,也叫爬山算法。可惜这种方法很容易受到步长限制,将答案局限在某一区间峰值中,无法跳出,导致无法到达全局峰值。

于是考虑将爬山的决策选取增加一些随机扰动,摆脱步长限制。这也就是我们模拟退火算法的原理。

先放一张经典原理图:

设初温为 \(T_c\)(一般为一个千位数,具体情况具体分析),温度变化率为 \(T_d\)(一般为 \(0.95\)\(0.99\) 之间,略小于 \(1\)),末温为 \(T_e\)(一般在 \(1e-6\)\(1e-15\) 之间,是一个极小的数)。

我们每次将温度乘上变化率,到达末温时停止。结合原理图可知,退火过程中,需要满足温度越高时,扰动越大,所选择的位置跳的范围越大,当温度逐渐降低时,扰动逐渐变小,所选择的位置跳的速度逐渐趋缓,最终稳定在最优解附近,到达全局极值。

实现流程

1. 准备退火

退火之前,我们需要先设置好合适的初温温度变化率末温以及退火次数

其中,初温末温最好根据具体退火时需要用到的温度范围来决定大小。初温决定开始时扰动的广度,末温决定最后扰动的精度。

温度变化率则决定了单次退火搜索极值的准确度。打个比方,如果温度变化率为 \(0.01\),那么精度必然是堪忧的。

而有时两个相近的极值离得非常远,却又要求需要其中一个极值(靠前的)时,决定了你会得到哪个极值的重要因素可能就成了随机的扰动。这时我们就需要尽量多次数的进行退火,对每次退火的结果取最优解。一般情况下,我们都会使用卡时的方法来决定退火的次数:

while ((double)clock()/CLOCKS_PER_SEC<0.95) x=sx,y=sy,tuihuo();

单次退火的程序框架如下:

void tuihuo(){
	double ct=2000,dt=0.97,et=1e-6;//!
	for (double t=ct;t>=et;t*=dt){
		//do something...
	}
}

接着我们再来谈谈对于每一个温度,我们需要怎么退火。


2. 进行退火

如果将所有状态的解抽象为一个多峰函数,设我们当前最优解状态 \(U\) 的横坐标为 \(x\),那么我们首先需要向左右随机找到一个横坐标为 \(x+aT\) 的候选解状态 \(V\) ,其中 \(a\) 是我们随机到的一个参数,\(T\) 是当前的温度。

找到 \(V\) 后,我们需要设置一个估价函数,对于两个状态进行估价,判断 \(U\)\(V\) 孰优孰劣。注意估价函数由于精度问题,一般会设置的比较大,这样可以比较出来所需精度较高的两个状态之间的优劣。若 \(V\) 更优,那么二话不说,将我们的当前状态设置为 \(V\),更新答案后变化温度;否则我们需要增加扰动,让当前状态有概率的变为 \(V\)(但最优解始终不更新).

那么扰动怎么设置呢?一般将某一温度的扰动大小估价设置为 \(\text{exp}(\frac{-\Delta e}{t})\),其中 \(\Delta e\) 为新解和当前解的差值。另一种写法是 \(\Delta e\) 为新解与最优解的差值。

为什么要这么设置呢?由退火原理知,我们希望温度越低,换的可能性越小;新值和答案差距越大,换的可能性也越小。而对于该扰动函数,温度与换的可能性成正比,当前估价与答案的差距和换的可能性成反比,和我们的希望相符。

于是估价设置就应运而生,为 \(\text{exp}(\frac{-\Delta e}{t})\)。将该值与随机概率比较,若大于随机概率,则更换当前状态,否则不换,此时就完全符合了上述的分析。


3. 调整参数

模拟退火是随机化算法,自然会存在着一定的错误率,于是就要用到一项十分重要的技术——调参!

由于我们找相近状态时寻找的范围是和 \(T\) 有关的,所以我们的初温末温的设置一定要迎合寻找范围的设置。不能设置了个初温为 \(1e4\),但实际上 \(100\) 以上的时候扩展的范围都是全图,这样的话前面一段温度的退火显然是意义不大的。当然也不能将初温设置的太低,导致一开始解就被局限在某一区间中调不出来。末温也是同理,决定了答案的精度。

而如果搜索时对答案的准确度要求不高,我们就可以适当调小温度变化率(如 \(0.8\) 左右?);反之则需要适当调高(如 \(0.995\)?)。

退火次数就不必说了,一般情况下 \(5\) 次左右就足够,但通常情况下我们更喜欢通过卡时来尽量增多退火次数。不过卡时的时候需要注意当题的给定时限,不要自以为是地直接设为 \(1s\).

还有一点就是随机数的选用,一般我们当然会选取:

srand(time(0));

但如果对自己的程序自信心不高或者想整活当然可以选取诸如 \(114514\) 之类的数当做种子,不过注意不要忘记

切记,一定要配合暴力对拍调参!!!


Example


1. P1337 [JSOI2004] 平衡点 / 吊打XXX

一个二维平面内有 \(n\) 个点,找出一个平衡点使得该点距离 \(n\) 个点的距离之和最小。

模拟退火能解决的一大类问题就是几何类问题,通常可以骗掉大多数几何题目。

我们可以在全局记录一个当前最优解以及最优解的坐标,每次从最优解向四周寻找候选解,然后通过对于两个解估价找出更优的解,按照正常流程决定是否更新即可。估价函数设置为到 \(n\) 个点的距离之和即可。

不过要注意一些细节:

  • 本题坐标范围不大,寻找候选解时 \(a\) 的值从 \(-1\)\(1\) 之间选择即可。
  • 本题答案精度要求不大,末温不必设置太低;为了准确度可以相应提高温度变化率。
double getsum(double x,double y){
	double sum=0;
	for (int i=1;i<=n;i++) sum+=sqrt((e[i].x-x)*(e[i].x-x)+(e[i].y-y)*(e[i].y-y))*e[i].w;
	return sum;
}
void tuihuo(){
	double ct=3000,dt=0.997,et=1e-6;
	for (double t=ct;t>et;t*=dt){
		double nx=zansx+((double)rand()*2/RAND_MAX-1)*t; 
		double ny=zansy+((double)rand()*2/RAND_MAX-1)*t;
		if (nx<minx||nx>maxx||ny<miny||ny>maxy) continue;
		double now=getsum(nx,ny);
		if (now<ans) ans=now,ansx=zansx=nx,ansy=zansy=ny;
		else if (exp((ans-now)/t)<rand()/RAND_MAX) zansx=nx,zansy=ny;
	}
}

2. P3878 [TJOI2010]分金币

除了几何类问题,模拟退火同样还可以解决一些其他问题。

对于本题,可以考虑先随便将金币分成数量满足要求的两组,退火时随机交换两个金币的组别,比较前后答案优劣。

实现上,可以考虑将一个数组的奇数位看作一组,偶数位看作一组,每次随机交换即可。

注意,本题的状态其实就是金币分配的方案,所以在寻找候选解的时候无需与 \(T\) 扯上关系,相邻状态其实就是交换任意两个金币之后的状态,直接随机下标后交换位置比较即可。

ll get(){
	ll ans1=0,ans2=0;
	for (int i=1;i<=n;i+=2) ans1+=a[i];
	for (int i=2;i<=n;i+=2) ans2+=a[i];
	return abs(ans1-ans2);
}
void tuihuo(){
	double ct=5000,dt=0.965,et=1e-10;
	for (double nt=ct;nt>et;nt*=dt){
		int x=rand()%n+1,y=rand()%n+1;
		swap(a[x],a[y]);
		ll now=get();
		if (now<ans) ans=now;
		else if (exp((ans-now)/nt)<(double)rand()/RAND_MAX){
			swap(a[x],a[y]);
		}
	}
}

3. P2503 [HAOI2006]均分数据

本题有很多玄学随机解法。。介绍两种。

解法1:模拟退火+dp

首先候选解的选取依旧可以采用交换两个位置的形式进行扩展,主要来考虑怎么设置估价函数。

发现当我们如果我们只将相邻的一段划为一组的话,其实就是一个关于连续分组的经典 dp 问题,将式子化简一下即可简单转移。

然后就是调参啦~

解法2:随机贪心

如果不考虑正确性的话,本题有一个很假的贪心解法,即每次选数时将其放入当前总和最小的组内,这样最后的方差一定是较优的。

但是这种解法一定不会过掉被出题人精心构造的数据。所以我们可以考虑将原序列随机打乱,再执行这种贪心,并通过卡时重复进行许多次,又由于 \(n,m\) 数据范围较小,所以可以顺利通过此题。

扩展的来讲,当某一题存在一种很假的贪心只能得出较优解,但贪心的正确性是和序列顺序相关,且序列长度较小时,就可以考虑使用随机打乱序列顺序+贪心的方式进行求解。这也称作【随机贪心】。

附上随机打乱序列的代码:

random_shuffle(a+1,a+1+n);

Problem


1. P2538 [SCOI2008]城堡

先考虑一个问题,如果我们知道了都有哪些点有城堡,该怎么求出每个城市到达城堡的最短距离呢?

很显然,将所有城堡加进优先队列后跑 dijkstra 即可。

那么剩下的问题就只有该如何确立剩下 \(k\) 个城堡的位置了。

由于 \(n\) 比较小,考虑模拟退火。

先随意选定 \(k\) 个非城堡点作为城堡,每次退火交换一对城堡点与非城堡点,估价函数设为跑一遍 dijkstra 之后的答案即可。


2. P3936 Coloring

考虑先随机分配将方格染色,然后开始模拟退火。

退火时随机交换两个方格的颜色,并计算出本次交换的贡献(两个位置颜色改变的贡献)。

对于调参,注意到本题 \(n\times m\) 的范围比较大,所以对于精度的要求比较高,需要将温度变化率设为一个及其接近 \(1\) 的数,如 \(0.99995\) 等。为了保证时间复杂度,我们自然需要相应将初温末温的范围稍微缩小,当然,末温也不能太高,\(10^{-10}\) 左右即可。

还有就是,仔细观察就会发现,若随机选择两个方格进行交换,答案很难变得更优,或者说,答案很难在每次交换都有提升,寻找最优解的过程可能是个弧形,也就是先交换几次下降,再交换几次上升,从而达到最优解。

这时如果我们在判定答案优劣的时候还是将新解和最优解比较而非和当前解比较的话,由于我们每次只交换一个位置,所以将会在象征优劣的多峰函数上无法跳出,被局限在一个范围内。此时将新解与当前解比较的优势就显现了出来——我们不一定要超过新解,只要比当前解优,就可以继续退火——从而大大降低了退火被局限的可能性。

所以以后写法就尽量改为将当前解和新解比较好了,最优解只起到记录答案的作用,吃一堑长一智(


3. P4212 外太空旅行

给定一张图,求出一个最大子图,使得子图内任意两点之间都有直接连边。这就是最大团问题

最大团问题是一个NPC问题,无法在多项式复杂度内求解。正常的求解方式就是枚举子集+判断是否可行,时间复杂度 \(O(2^nn^2)\)

而本题 \(n\le50\),该时间复杂度显然行不通。

于是考虑随机化算法。依然有两种解法。


1. 模拟退火+判断

考虑每次退火时随意交换两个元素的位置,然后对当前序列从前到后判断是否可以加入最大团,如果可以则加入,不行则跳过,即可得到当前序列的估价,然后正常流程解决即可。

2. 随机贪心

从模拟退火的过程可以看出,真正有用的要素也就是序列的顺序,只要确定了顺序自然可以求解。所以直接上随机化贪心,随机打乱序列顺序后求解即可。


除此之外,还有一个地方可以优化。注意到 \(2^{50}\) 是在 long long 范围之内的,所以可以用一个 long long 变量表示每个节点的连边情况,这样一来,原本 \(O(n)\) 判断可行性的复杂度就可以被我们用位运算操作 \(O(1)\) 解决,为跑贪心腾出了时间,大大提升了程序的正确性。


Exercise

SP34 RUNAWAY - Run Away

P1248 加工生产调度

CF1105E Helping Hiasat

AT5141 [AGC035D] Add and Remove

P5544 [JSOI2016]炸弹攻击1


Ending

其实仔细思索,模拟退火的本质和二分其实十分相似,都是将最优性问题转化为判定性问题

只不过它更多的是利用题目较小数据范围,通过在当前状态附近随机判定对象,以及通过合理的退火逐渐降低寻找范围,使得答案逐渐稳定在正解附近,较大概率的得到正解。

这就启发我们,如果在解决最优性问题时没有头绪,完全可以考虑将题目转化为判定性问题。且当题目数据范围较小时,可以考虑采用退火或者随机贪心,通过 rp 暴力解决(

再总结一下需要注意的地方以及一些小 tip:

  1. 注意判断扰动时不等号的方向!!!!!1
  2. 如果题目对于精度要求较高,比如数据范围较大(如 P3936),就需要适当调大温度变化率,到 \(0.999\) 以上。相应的,需要调宽卡时时限。
  3. 一般情况下模拟退火由于退火时的优秀性质,是会优于随机贪心的,所以还是尽量使用模拟退火,退火无思路了再考虑随机贪心。
posted @ 2022-08-16 14:09  ydtz  阅读(276)  评论(0编辑  收藏  举报