浅谈欧洲算法——模拟退火

风吹夏天 2020-01-24

初听说退火这个名词感觉就很(zhuang)帅(A__CDEFG...)

直到学了退火之后,我才发现:

退火不只是帅,而且非常万能

甚至比 D (大) F (法) S (师)还要万能

简直就是骗(de)分神器啊

简介

作为一个计算机算法,它竟然在百度上有物理词条!

浅谈欧洲算法——模拟退火

当时我看了就懵了,你说计算机一个算法,跟冶炼金属有什么关系啊?

后来我看了算法的词条...

浅谈欧洲算法——模拟退火

是不是更懵了...

方便大家理解(变得更懵),我搬了百度上的定义:


Simulate Anneal Arithmetic (SAA,模拟退火算法)
根据Metropolis准则,粒子在温度T时趋于平衡的概率为e-ΔE/(kT),其中e为温度T时
的内能,ΔE为其改变量,k为Boltzmann常数。用固体退火模拟组合优化问题,将内能E模拟为目标函数值f,温度T演化成控制参数t,即得到解组合优化问题的模拟退火算法:由初始解i和控制参数初值t开始,对当前解重复“产生新解→计算目标函数差→接受或舍弃”的迭代,并逐步衰减t值,算法终止时的当前解即为所得近似最优解,这是基于蒙特卡罗迭代求解法的一种启发式随机搜索过程。退火过程由冷却进度表(Cooling Schedule)控制,包括控制参数的初值t及其衰减因子Δt、每个t值时的迭代次数L和停止条件S。

(懒的话不看也罢,本来就是拿来凑字数的)


说实话看完还是挺清楚(?)的,总计一下就是:

通过不断产生随机新解,并且比较新解与目前最优答案的差距大小;若新解较当今最有更优,就在新解的附近再产生下一个新解。通过大量的循环,从而达到引导答案最终趋向于标准最优解的目的。

稍微有点感觉了吧。

再说简单一点就是:

随便乱蒙答案,如果感觉蒙的比较好,就在它附近继续蒙

只要你蒙的足够多,你总能蒙到正确答案

...

(其实好像跟金属退火还没啥关系,没事 右上角洛谷日报2018: 浅谈玄学算法——退火 接着往下看)

->continue

实现过程

既然是退火,那就得有火吧。

嗯...

对不起

这个还真没火(打开电脑主机,你会在你的CPU上看到火

在计算机里面,退火过程主要用的是三个参数:初始温度T , 每次退火后温度变化量delta , 结束温度t 。其中a是一个略小于1的数。

说明一下这三个变量的用途吧,其实了解了用途之后应该也就能懂退火的原理了。

初始温度T:

这个主要决定的是我们随机蒙答案的范围。T越大,我们的随机答案就会分布地越广。主要保证的是算法的答案不会因为数据的偏移(过大或过小)而漏掉可能的正解。

只要是数据,T就一视同仁 。

结束温度t :

这个决定我们算法最后的精度。t越小,我们最后生成随机答案的范围就会越小。主要防止因为随机范围过大而造成的大量无意义的随机,能够保证我们最后的答案与标准答案相差无几(基本会被测试点无视或超出输出要求的精度),举个形象的栗子,我们t一般取的是1e-15(10的-15次方)。这里答案即使与标准答案有差,又有拿到题目会让你输出这种精度呢?

变化量delta(我不会打那个数学符号...):

T让我们随机的范围要广,t又说会把随机定到一个很小的范围。那到底随机随什么啊!

这个时候delta就出现了,它的作用就是在每次随机后让温度减小那么一点点(真的只是一点点)。让我们的随机从开始追求广度的T逐渐变化成强调细节的t。说白了就是让我们的退火既有T的广度,不会漏答案;又有t的精度。

看都这里可能又有人会懵了。你随机既要枚举广度,后面又要缩小范围。我咋知道所的范围是不是对的。没准你在错的答案附近随机蒙半天呢?

这个问题的答案其实就是退火算法的精髓:

对于随机的每一个答案:如果它比当前最优解更优,则接受它为当前最优解;若不是,则以一个跟当前温度有关的概率接受它。

->continue

理解

第一个人疑问可能所有看到这里的人都会问。

Q: 在随机时如果产生了一个并不是当前最优的新解,为什么不是直接开始下一轮的随机,而是以一个跟当前温度有关的概率接受它呢?

嗯...这个其实根本没法解释 挺好解释的。因为大家都知道,当前最优其实并不代表整体最优。因此我们在随机产生新解时,即使这个解是当前最优,我们仍然无法保证它会是整体最优。而我们每次枚举的范围是跟t相关的,所以其实开始并不是枚举所有可能的解;而是枚举一个范围超级大的解(在当前最优的基础上上下浮动一个关于t的函数)。而标准最优解可能与当前最优解位置或是数量相差较大。大到一次随机也无法达到那个范围。因此我们需要一些数作为中间过度。以让我们的随机能够随到整体最优解,而不会被 恶意卡数据的出题人陷害 当前最优蒙蔽了双眼。

所以是时候完整讲一下这个算法了;

通过随机rand函数,在当前的局部最优解上随机上下浮动(浮动幅度与当前温度成正相关)产生新解。对于每一个新解,若它较当前更优。则无条件接受它为当前最优解,否则则以一个概率(概率大小仍然与当前温度成正相关)来接受它。每一次随机结束后,将当前温度减小一点。

估计如果认真看了前面的人应该都 自闭 理解了。

它非常鲜明地体现了计算机的风格( 简单粗暴,以速度求质量 )

当然又有人要会问了。

Q: 既然在高温时接受当前不那么优的解的概率也趋近于1,也就是基本都接受了。那么凭什么最后能得到最优解呢?

其实我之前在学退火时也有这个问题。这个问题的答案其实就是退火算法之所以能得到最优解的理论依据。

因为如果出现更优解,我们是不考虑概率地无条件接受的;因此一个解越优,它被接受的总概率就会越大(虽然可能只会比次优解大0.00001%)。但是我们通过数百万次的随机运算不断地去扩大这个概率差(扩大是指数级增长的)。最终使得最优解的存活概率接近1。

这个随机到底随了多少次呢?

每一道题都有不同的次数,但大部分题的数量级是差不多的(程序时间就那么1000ms)。

就以P2210 Haywire例题来解答(其实我之前也不清楚)。我在我的代码里面加了一个countt变量,每一次随机运算它就自增一次,最后它的值有多大呢?

浅谈欧洲算法——模拟退火

(17是答案,自己无视掉就行了)

足足有2357060!!!

值得一提的是我这题用的参数:

初始温度:3000

结束温度:1e -17( 10的负17次方 )

每次随机结束后温度的变化值:0.996( 温度 * 变化值 = 新温度 )

退火次数:20 ( 就前面三个参数重复调用了20次 )

其实也不必太吃惊,基本所有题目退火用的参数都是这几个( 可能次数会有点差异 )

->continue

例题

( 还是放例题讲容易点 )P1337 平衡点

作为一道纯正的几何物理题,竟然不给物竞的人做...

这题用模拟退火来做是考虑因为这题的最后解只有一个,况且比较好验证解(O n 复杂度还可以接受);并且如果用浮点运算的话可能会造成精度损失,最终影响答案。

#include<iostream>
#include<stdlib.h>
#include<cmath>
#include<cstdio>
#define cold 0.996 // 降温系数
#define temperature 3000 // 初始温度
#define time 4 // 退火次数( 这种纯物理题不用太多次 )
#define INF 999999
using namespace std ;
struct spot{
    int x ;
    int y ;
    int weight ;
};
spot map[1001] ;
int n ;
double t ;
double tmpx , tmpy , tmpw ;
double ansx , ansy , anse ;
//double nowx , nowy , nowe ;
double energy ( double x , double y ){ // 判断当前点的能量,能量越小越稳定
    double e = 0 ;
    for( int i = 1 ; i <= n ; i ++ ){
        tmpx = x -map[i].x ;
        tmpy = y - map[i].y ;
        tmpw = map[i].weight ;
        e += sqrt( tmpx * tmpx + tmpy * tmpy ) * tmpw ;
    }
    return e ;
}
void colddown(){
    t = temperature ;
    while( t > 1e-15 ){ // 开始模拟退火
//      cout << "debug" << " " << t << endl ;
        double nowx = ansx + ( ( rand() * 2 ) - RAND_MAX ) * t ; // 随机产生一个值,使点位移
        double nowy = ansy + ( ( rand() * 2 ) - RAND_MAX ) * t ;
        double nowe = energy( nowx , nowy ) ;
        double de = nowe - anse ;
//      cout << nowx << " " << nowy << endl ;
        if( de < 0 ){ // 如果当前解更优,就直接接受
            ansx = nowx ;
            ansy = nowy ;
            anse = nowe ;
//          cout << anse << endl ;
        }
        else{
            if( exp( -de / t ) * RAND_MAX > rand() ){ // 否则以一个概率接受
                ansx = nowx ;
                ansy = nowy ;
            }
        }
        t *= cold ;
    }
}
void solve(){
    for( int i = 1 ; i <= time ; i ++ ){
        colddown() ; // 多来几次,保证结果正确性
    }
}
int main () {
    srand( 201821307 ) ; // 一个好的随机数种子很重要
    cin >> n ;
    for( int i = 1 ; i <= n ; i ++ ){
        cin >> map[i].x >> map[i].y >> map[i].weight ;
        ansx += map[i].x ;
        ansy += map[i].y ;
    }
    ansx /= n ;
    ansy /= n ;
    anse = energy( ansx , ansy ) ;
    solve() ;
    printf( "%.3lf  %.3lf" , ansx , ansy) ;
    return 0 ;
}

( 注释掉的那几条是调程序时用的 )

其实这道题应该也不用多说,如果理解了前面的原理基本就不难懂。

( 竟然已经水了200多行 )

->continue

适用范围

虽然退火算法看起来好像非常暴力万能,但其实根dfs一样,也是有一定适用范围的。

  1. 答案较为简单,不许要输出过多数据的题目

  2. 能够在比较短的时间内验证答案( NP类问题都可以试试 )

  3. 不同的解之间有明显的互相转化的关系,及可以通过某个随机操作使一个解转化为另一个解

  4. 解集一半只有 1~3 个峰值,整体上呈现为数个单调函数

  5. 自己脸够白非酋落下了WA的眼泪

最后要补充的一点是,可能你的算法并没有任何问题,但你题目就是A不了。其实这是退火算法造成的,毕竟是随机算法。如果退火的三个系数或是随机种子用的不够好。WA也是很正常的。

浅谈欧洲算法——模拟退火

(比如这题我并没有改任何的代码,只是用不同的参数去退火,结果却能造成不同的分数 )

最后只能祝大家找的种子全部Ac 脱欧入非