• 【算法】模拟退火解析


    模拟退火 (Simulated annealing) ,简称 (SA) ,最早在 (1953) 年由 (N. Metropolis) 提出,后经优化得到现在广泛应用的算法,应用在很多领域当中。

    算法思想

    模拟退火是随机化搜索的一种,若随机化搜索写得好,则可以实现高效率和答案的正确率高(虽说不是 100%100% )。很多时候在想不出解决办法,或方法的时间复杂度出现极大情况时,可使用模拟退火。所说是有较大几率正确,但还是有疏漏,那么可以多次试验来更加接近准确地求出这个值(还是要看运气)。

    模拟退火,顾名思义,是模拟工业上固体降温的过程。先将固体加温到一定的温度后,在按照适当的温度进行冷却,冷却到改物体想要达到的状态。温度降低地越慢,则该物体的质量约高,因为分子在因热加速运动中找到了更加合适的位置。当温度逐渐降低,分子运动减缓,达成目的。

    那么这一现象被科学家与计算机算法所联系起来,就成了现在的模拟退火。

    网上的一张图,模拟退火可视化:(经典示意图)

    模拟退火有几个很关键的参数,这几个参数决定了模拟退火的优劣。

    1. 随机种子(seed),可以使用 (19260817) ,或是时间,不推荐使用其他参数,很可能会降低正确率。
    2. 初始温度 (Temp High),一般取 (100)(10000) 不等,但作者更加倾向于 (2000)(3000) 的数字。
    3. 目标温度 (TempLow) ,一般取 (1^{-10})(1^{-15})
    4. 温度变化率 (Templess),一般取0.99至0.9999。建议不取太大,效率不高。
    #define Seed 19260817
    #define TempLess 0.9975
    #define TempHigh 2879.0
    #define TempLow 1e-12
    

    来看看降火的主体部分。

    void SA() {
        double temp = TempHigh; // 初始化温度
        定义初始状态
        while (temp > TempLow) { // 直到降温条件
            double nowans = Get_Ans(当前状态); // 更新最优解
            double diff = nowans - ans; // 与现在答案的差值
            if (diff > 0) { // 比当前答案更优
                转移状态
                ans = nowans; // 更新答案
            } else if (exp(-diff / temp) * RAND_MAX < rand()) {
                // 至于为什么这样写请见例题部分
                转移状态;
            }
            temp *= TempLess; // 降温
        }
    }
    

    模拟退火查询的是多峰函数的最值。

    以下曲线是解析式为 (y=0.05x^3-0.5x^2)的函数的图像:


    先来考虑贪心的做法:

    当到达点 (A) 时,程序会选择更高的一个点,那么会从 (A) 点到达 (B) 点,而再从 (B) 点俯畋,看到了点 (C) ,由于

    (C) 的纵坐标比 (B) 小,所以点 (B) 不会到达点 (C) 。换句话说,该程序 (100\%) 不会接受点 (C) ,进而更不会到

    达点 (D) 。不难发现,这时候找到的局部最优解并不是全局最优解。

    而模拟退火再次做岀了改进。假设初始位置在点A,则会基于 (A) 做出左右摆动,经过数次摆动后到达(B)

    。再进一步摆动,假设摆动到了 (C) 点,但是 (C) 的纵坐标比 (B) 小,会以一定几率以 (C) 的纵坐标来接受(C)

    。进而在以同样的方式摆动到点D,找到更高点。

    由于是该算法随机性较高,所以多跑几遍该函数。

    下面结合一道例题更加深入地探究,P5544 [JSOI2016]炸弹攻击1

    题意:

    (n) 个圆, (m) 个点,请求出一个半径不超过 (r) 的圆,使得与这 (n) 个圆没有交集,且能够覆盖的点最大。

    思路:

    此题的答案圆的圆心并不满足是整数,且由横纵坐标两个值来影响,并不具有规律。这样的问题通常使用模拟退火来解决。

    初始状态包含了横坐标和纵坐标,为了提高正确率与效率,设为所有点的横纵坐标的平均值。

    GetAns 函数也很简单,先确定半径,半径为这个点到各个圆的切线的距离的最小值,即两个圆心的距离减去当前枚举到的这个圆的半径。后枚举每个点,若这个点被覆盖则 res++ ,最后返回 (res)

    double Get_Ans(double x, double y) {
        double ans = 0;
        double rkill = r ;
        for (int i = 1; i <= n; ++)
            rkill = Min(rkill, Dist_Cartesian(XC(i), YC(i), x, y) - RC(i));
        for (int i = 1; i <= m; ++i)
            if (Dist_Cartesian(XE(i), YE(i), x, y) <= rkill)res += 1.0;
        return ans;
    }
    

    主体部分

    void SA() {
        double tmp = TempHigh, ansx = initx,
               ansy = inity; // 降温前初始化
        while (tmp > TempLow) {
            double nowx = ansx + ((rand() << 1) - RAND_MAX) * tmp;
            double nowy = ansy + ((rand() << 1) - RAND_MAX) * tmp;
            double nowans = Get_Ans(nowx, nowy);
            double diff = nowans - ans;
            if (diff > 0) {
                initx = nowx, inity = nowy;
                ansx = nowx, any = nowy;
                ans = nowans;
            } else if (exp(-diff / tmp) * RAND_MAX < rand()) {
                ansx  = nowx, ansy = nowy;
            }
            tmp * TempLess;
        }
    }
    

    首先来看这段代码

    double nowx = ansx + ((rand() << 1) - RAND_MAX) * tmp;
    double nowy = ansy + ((rand() << 1) - RAND_MAX) * tmp;
    

    由答案左右摆动,生成新的当前状态 (nowx)(nowy) ,摆动幅度是随机的,应该是由分子做无规则运动而来。乘上 (tmp) 当前温度是由分子在越热的环境中,运动得越快而得来。

    紧接着两行就是求出当前状态的答案,在求出它与当前最优解的差值。

    第一个 (if) 是当前这个局部解大于当前最优解,则用当前最优的局部解来更新最优解。

    重点是下一个 (if) ,这行代码就是它与贪心的不同,以一定几率接受这个解,在用它更新当前状态,进行左

    右摆动,从而找到局部更优解,更加接近整体最优解。其条件的优越性由 (Metropolis) 接受准则给出。也就是 (else if) 中的条件:

    exp(-diff / tmp) * RAND_MAX < rand()
    

    思路整理完了,此题并没有多少思维难度,但是需要对上述几个参数进行调整,可以多总结一些正确率大的参数,以备下次使用

    完整 C++ 代码:代码书写来自 Last_Breath

    #include <cmath>
    #include <cstdio>
    #include <cstdlib>
    #define Min(a, b) ((a) < (b) ? (a) : (b))
    #define Seed 19260817//随机种子
    #define TempLess 0.9975//温度变化率
    #define TempHigh 2879.0//初始温度
    #define TempLow 1e-12//目标温度
    void Quick_Read(double &N) {//double快速读入
        N = 0.0;
        double now, wei = 0.1;
        bool op = false;
        char c = getchar();
        while (c < '0' || c > '9') {
            if (c == '-')
                op = -1;
            c = getchar();
        }
        while (c >= '0' && c <= '9') {
            N = N * 10.0 + (c ^ 48) * 1.0;
            c = getchar();
        }
        if (c == '.') {
            c = getchar();
            while (c >= '0' && c <= '9') {
                N += (c ^ 48) * wei;
                wei /= 10.0;
                c = getchar();
            }
        }
        if (op)
            N = -N;
    }
    const int MAXN = 15;
    const int MAXM = 1e3 + 5;
    struct Circle {//题目中的圆
        double Abscissa_C, Ordinate_C, Radius_C;
    #define XC(x) buildings[x].Abscissa_C
    #define YC(x) buildings[x].Ordinate_C
    #define RC(x) buildings[x].Radius_C
    };
    Circle buildings[MAXN];
    struct Enemy {//题目中的点
        double Abscissa_E, Ordinate_E;
    #define XE(x) foe[x].Abscissa_E
    #define YE(x) foe[x].Ordinate_E
    };
    Enemy foe[MAXM];
    double n, m, r;
    double initx, inity;//记录答案的横纵坐标
    double ans;//答案
    double Dist_Cartesian(double XA, double YA, double XB,
                          double YB) {//两点间距离公式
        double frontx = (XA - XB) * (XA - XB);
        double fronty = (YA - YB) * (YA - YB);
        double dist = sqrt(frontx + fronty);
        return dist;
    }
    double Get_Ans(double x, double y) {//找到当前状态的答案
        double res = 0;
        double rkill = r;
        for (int i = 1; i <= n; i++) //求出最大半径
            rkill = Min(rkill, Dist_Cartesian(XC(i), YC(i), x, y) - RC(i));
        for (int i = 1; i <= m; i++) //求出被圆覆盖的点
            if (Dist_Cartesian(XE(i), YE(i), x, y) <= rkill)
                res += 1.0;
        return res;
    }
    void SA() {
        double temp = TempHigh, ansx = initx, ansy = inity;//初始化
        while (temp > TempLow) { //降温
            double nowx = ansx + ((rand() << 1) - RAND_MAX) * temp;//当前状态x
            double nowy = ansy + ((rand() << 1) - RAND_MAX) * temp;//当前状态y
            double nowans = Get_Ans(nowx, nowy);//当前局部答案
            double diff = nowans - ans;//当前答案与最优解的差值
            if (diff > 0) { //比当前最优解更优则更新最优解
                initx = nowx;
                inity = nowy;
                ansx = nowx;
                ansy = nowy;
                ans = nowans;
            } else if (exp(-diff / temp) * RAND_MAX < rand()) {
    //按照Metropolis接受准则接受改状态
                ansx = nowx;
                ansy = nowy;
            }
            temp *= TempLess;//降温
        }
    }
    void Cool_Down() {
        int frequ = 6;
        while (frequ--) //随机化算法尽量多跑几次
            SA();
    }
    void Make_Seed() {//生成随机种子
        srand(Seed);
    }
    void Read() {//输入
        Quick_Read(n);
        Quick_Read(m);
        Quick_Read(r);
        for (int i = 1; i <= n; i++) {
            Quick_Read(XC(i));
            Quick_Read(YC(i));
            Quick_Read(RC(i));
        }
        for (int i = 1; i <= m; i++) {
            Quick_Read(XE(i));
            Quick_Read(YE(i));
            initx += XE(i);
            inity += YE(i);
        }
        initx /= m;//以平均值开始提高效率与准确率
        inity /= m;
    }
    void Write() {//输出
        printf("%.0lf", ans);
    }
    int main() {
        Make_Seed();
        Read();
        Cool_Down();
        Write();
        return 0;
    }
    

    The desire of his soul is the prophecy of his fate
    你灵魂的欲望,是你命运的先知。

  • 相关阅读:
    Android TabHost(选项卡)
    监控工具之---Prometheus查询持久性(六)
    监控工具之---Prometheus表达式promQL生产中应用(五)
    Grafana Configuration 参数详解(1)
    监控工具之---Prometheus数据可视化Grafana(七)
    监控工具之---Prometheus 安装详解(三)
    监控工具之---Prometheus 配置exporter四)
    Kubernetes容器编排技术---kubectl命令行工具用法详解(三)
    Kubernetes容器编排技术---Kubernetes基于kubeadm安装与配置(二)
    Azure Iaas基础之---创建虚拟机
  • 原文地址:https://www.cnblogs.com/RioTian/p/14772397.html
Copyright © 2020-2023  润新知