前置技能——爬山:https://www.cnblogs.com/AKMer/p/9555215.html
模拟退火是一种非常好的随机化算法,是爬山算法的改进版,它的灵感来源于金属冶炼退火,和遗传算法一样,也是一种大自然馈赠给我们的自适应随机化算法。
有兴趣的同学可以去看看遗传算法:https://www.cnblogs.com/AKMer/p/9479890.html
关于模拟退火,牵扯到物理学的一些知识,有兴趣的同学们可以去看百度百科。
百度百科:https://baike.baidu.com/item/%E6%A8%A1%E6%8B%9F%E9%80%80%E7%81%AB/8664695?fr=aladdin
模拟退火基本概念
\(T\):初始温度。
\(T_0\):结束温度。
\(\Delta T\):温度变化系数,一般在\([0.950,0.999]\)之间,每次降温温度都乘以\(\Delta T\)。
\(calc(x)\):状态\(x\)所对应的权值。在讲爬山算法的时候我们就提到过了,模拟退火也将状态空间里每一个状态映射成一个个点,而状态的优秀度就是一个个点对应的函数值。
温度一般为double类型,由高温逐渐降到低温。
模拟退火与爬山的区别
爬山算法是一种鼠目寸光的贪心,只会往当前优秀的邻居状态转移。我们只能通过增加爬山的人数来覆盖尽量多的状态空间以保证算法效率。
然而模拟退火却不一样,对于一个比当前状态更加优秀的状态,我们可以毫不犹豫地转移过去。
但是如果该邻居状态不比当前状态优秀,我们也不会一棒子打死,而是根据物理学上一些知识来计算转移概率。
然后在信息学领域内,我们只要记住一件事情就好了。那就是:
在温度为\(T\)时,当前状态\(A\)转移到一个不优于它的状态\(B\)的概率是\(exp(\frac{calc(B)-calc(A)}{T})\)
别问我为啥去问物理学家去……总之按这个概率去转移可以大大增加遍历到代表正解的状态的几率。
根据题意我们有时需要将\(calc(B)-calc(A)\)改成\(calc(A)-calc(B)\),因为模拟退火算法需要满足一条性质,那就是“在温度不同的情况下,温度越低时发生同一个转移的概率就越低”,也就是当\(calc(B)-calc(A)\)为定值的时候,\(T\)越小概率也越小,所以分子要保证是负数。
显然,这个概率是属于区间\([0,1)\)的。
记得找邻居节点的时候灵性一点,不要死死板板找挨在一起的,不然我前脚往一个差一点的状态走了后脚就走回来了。还有就是更改状态可以借鉴遗传里的变异。然后其余的跟爬山就没啥区别了。
说实话,会了退火之后感觉爬山就没什么卵用了……
求函数\(f(x)\)峰值代码
#include <ctime>
#include <cstdio>
#include <algorithm>
using namespace std;
#define ll long long
const double T_0=1e-5;
const double del_T=0.998;
double T=1e5;
ll f(int x) {
//返回函数值
}
int main() {
srand(time(0));
int pos=rand()-RAND_MAX/2;ll ans=f(pos);
while(T>T_0) {
int x=pos+T,y=pos-T,nxt;//所谓的灵性找邻居法
if(f(x)>f(y))nxt=x;
else nxt=y;//找下一个点
if(f(nxt)>f(pos)||exp((f(nxt)-f(pos))/T)*RAND_MAX>rand())
pos=nxt;//如果下一个点比当前点高或者在一定的概率之内我们就转移
ans=max(ans,f(pos))
T*=del_T
}
printf("%lld\n",ans);
return 0;
}//没有什么是退一万次火解决不了的。如果有,那就再退一万次。
注意:上面这个乱跳的代码没有啥正确性可言,只是意思意思一下而已,不要当真。
关于爬山和模拟退火,网上流传着这么两句比喻:
爬山算法:兔子朝着比现在高的地方跳去。它找到了不远处的最高山峰。但是这座山不一定是珠穆朗玛峰。这就是爬山算法,它不能保证局部最优值就是全局最优值。
模拟退火:兔子喝醉了。它随机地跳了很长时间。这期间,它可能走向高处,也可能踏入平地。但是,它渐渐清醒了并朝最高方向跳去。这就是模拟退火。
不知道你在阅读了我的两篇博客之后,有没有这种感受。如果没有,那……
肯定是我太菜写的太渣了。