首页 > 技术文章 > [算法笔记]模拟退火

NP2Z 2021-09-23 06:34 原文

模拟退火

之前用了好长好长时间才发现自己打的其实是一个随机化算法,而不是我们整天所说的 模拟退火

模拟退火真的是一个很神奇很神奇的算法,并不是简单的随机化。

就比如说你并不知道为什么他就和 \(e\) (自然对数)扯上关系。。。

这个算法一般适用于求一个最优解的问题。

我们通常使用贪心算法配合这个退火使用。

模拟退火简介

借用Flashhu大佬的话:

科学家们想到了物理的退火降温的过程——

一个处于很高温度的物体,现在要给它降温,使物体内能降到最低。

我们常规的思维是,越快越好,让它的温度迅速地降低。

然而,实际上,过快地降温使得物体来不及有序地收缩,难以形成结晶。而结晶态,才是物体真正内能降到最低的形态。

正确的做法,是徐徐降温,也就是退火,才能使得物体的每一个粒子都有足够的时间找到自己的最佳位置并紧密有序地排列。开始温度高的时候,粒子活跃地运动并逐渐找到一个合适的状态。在这过程中温度也会越降越低,温度低下来了,那么粒子也渐渐稳定下来,相较于以前不那么活跃了。这时候就可以慢慢形成最终稳定的结晶态了。

那么,我们可不可以把找到最优解,与形成结晶态,这两个过程联系在一起呢?

于是,模拟退火诞生了

似乎并没有什么关系,但是也是一种很好的比喻。。。

模拟退火算法

所以这样的话,我们就需要设置几个变量:

\[T(\text{起始的温度})\\ \Delta T(\text{温度的变化量})\\ Curans(\text{当前的答案})\\ \]

我们假设现在我们需要求解一个最小值的问题,那么如果当前我们取到的 \(f(x_{rand}) < curans\),那么我们现在就要取得这个 \(f(x_{rand})\) ,但是如果大于呢,我们并不是完全舍去他,而是在一定程度上去接受他。

这个概率就是

\[e^{\frac{-delta}{T}} * RAND\_MAX > rand() \]

这个关于自然对数的公式我也不知道是怎么来的,总之很准就对了。

就好比 \(59\) 场的第一个题目,我们是如果仅仅使用 \(rand()\) ,那么我们会发现这个尝试的答案非常均匀,我本以为这样很好,然而这个样子往往得不到最优的答案。

但是如果我们使用模拟退火算法,我们发现起始尝试的位置并不是十分均匀,但是我们可以发现在答案处左右尝试了几百次之多,这就是模拟退火算法的优势之处。

\(code\;that\;is\;from\;the\;59's\;T1\)

#include<bits/stdc++.h>
using std::cout; using std::endl;
#define try(i,a,b) for(register int i=a;i<=b;++i)
#define throw(i,a,b) for(register int i=a;i>=b;--i)
#define go(i,x) for(register signed i=head[x],y=edge[i].ver;i;i=edge[i].next,y=edge[i].ver)
namespace xin_io
{
	#define file(a) FILE *FI = freopen(#a".in","r",stdin); FI = freopen(#a".out","w",stdout);
    #define debug cout<<"debug"<<endl
    #define jb(x) cout<<#x" = "<<x<<endl
    #define sb(x) cout<<#x" = "<<x<<' '
    #define gc() p1 == p2 and (p2 = (p1 = buf) + fread(buf,1,1<<20,stdin),p1 == p2) ? EOF : *p1 ++
	char buf[1<<20],*p1 = buf,*p2 = buf;
    class xin_stream{public:template<typename type>xin_stream &operator >> (type &s)
    {
        s = 0; register bool f = 0; register char ch = gc();
        while(!isdigit(ch)) f |= ch == '-',ch = gc();
        while( isdigit(ch)) s = (s << 1) + (s << 3) + (ch xor 48),ch = gc(); return s = f ? -s : s,*this;
    }}io;
}
#define int long long
using namespace xin_io; static const int maxn = 1e6+10,llinf = 1e18+10;
//auto abs = [](int x) -> int{return x < 0 ? -x : x;};
namespace xin
{
	inline int abs(int x){return x < 0 ? -x : x;}
	int n,mid,ans = llinf*2;
	int a[maxn],temp[maxn];
	auto check = [](int x)
	{
		try(i,1,n) temp[i] = a[i] + abs(i - x);
		std::nth_element(temp + 1,temp + mid,temp + n + 1);
		register int goal = std::max(temp[mid],std::max(x,n-x+1));
		int ret = 0;
		try(i,1,n) ret += abs(temp[i] - goal);
		return ret;
	};
	inline short main()
	{
		file(c);
		io >> n; mid = (1 + n) >> 1;
		try(i,1,n) io >> a[i];
		int curans = check(mid),cur = mid;
		double t = 100000;
	
		auto getrnd = []() -> double
		{return (double)(rand() - rand()) / (double)(RAND_MAX);};

		while(t >= 1e-7)
		{
			register int x = cur + t * getrnd();
			x = std::max(1ll,x); x = std::min(n,x);
			int temp = check(x);
			int delta = temp - curans;
			if(delta < 0 or exp(-delta/t) / (RAND_MAX) > rand())
				curans = temp,cur = x;
			ans = std::min(curans,ans);
			t *= 0.98;
		}
		cout<<ans<<endl;
		return 0;
	}
}
signed main(){return xin::main();}

推荐阅读