Luckyleaves's Blog

stay hungry,stay foolish greedy and lucky

模拟退火

很久之前学的了,这次算是补个漏

简介

  • 退火,来自于物理,模拟退火算法来源于固体退火原理,是一种基于概率的算法,将固体加温至充分高,再让其徐徐冷却,加温时,固体内部粒子温升变为无序状,内能增大,而徐徐冷却时粒子渐趋有序,在每个温度都达到平衡态,最后在常温时达到基态,内能减为最小。

  • 模拟退火(下文简称退火)是一种高效的骗分算法,以简单易背的模板和优秀的效率闻名于世(就比暴力好一点),常常用于求解答案是连续不断(接近也行)的函数的最大值或最小值,强于三分,可求解多峰函数。

  • 其运行效果如图所示:

1.png

正确性

  • 说到随机算法,就一定要考虑其的正确性,退火算法理论上是一种贪心算法,它会尽可能找到离自己更近,且更接近正解的答案的点,然后跳过去。但不同于贪心算法的是,它有一定概率越过这个点,去其他峰试试运气,但越到后期越不容易跳过去,就越趋近稳定。所以,可以肯定的是,它不会只停留在一个峰上,而是会四处寻找其他答案。
  • 我们设每一次错误的概率为$0.9999$,但是当我们在这次退火的基础上再加上$50000$次退火,正确性就为:$0.9999^{50001} = 0.0067$ 就是一个极小的概率了,所以退火是一个稳定的正确的算法。

具体实现

  • 当我们理解到这个算法是随机的算法时,一定要充分运用到随机的艺术,对于初始温度和结束温度,还有温度变化率一定要在平日里积累经验,这里给出核心代码:
1
if(exp(-delte / t) < (double)rand() / RAND_MAX)//delte为上一个解和当前解的差值

例题

  • 这里以luogu P2503 [HAOI2006]均分数据 为例来简单介绍退火的运用

  • 根据题目,不难发现这是DP,但当我们在考场上不会的时候该怎么办呢?根据题目,我们可以发现,当我们更改一个值后,答案并不会增大很多,所以我们可以通过模拟退火解决此题

  • 代码:

  • #include<iostream>
    #include<cstring>
    #include<cstdio>
    #include<cstdlib>
    #include<cmath>
    #include<algorithm>
    #include<ctime>
    #define x first
    #define y second
    #define PII pair<int, int>
    using namespace std;
    const int N = 25, M = 10, Inf = 0x3f3f3f3f3f;
    int n;
    int w[N], q[N], s[M];
    double ans = 1e8;
    int m;
    int calc()//求解单次答案
    {
        memset(s, 0, sizeof(s));
        for(int i = 0;i < n;i ++)
        {
            int k = 0;
            for(int j = 0;j < m;j ++)
            {
                if(s[j] < s[k]) k = j;
            }
            s[k] += w[i];
        }
        double avg = 0;
        for(int i = 0;i < m; i ++) avg += (double)s[i] / m;
        double res = 0;
        for(int i = 0;i < m;i ++) res += (s[i] - avg) * (s[i] - avg);
        res = sqrt(res / m);
        ans = min(ans, res);
        return res;
    }
    void sim()//退火
    {
        random_shuffle(w, w + n);
        for(double t = 1e4;t > 1e-6;t *= 0.99)
        {
            int a = rand() % n, b = rand() % n;//随机我们交换的数
            double x = calc();
            swap(w[a], w[b]);
            double y = calc();
            double delta = y - x;
            if(exp(- delta / t) < (double)rand() / RAND_MAX) swap(w[a], w[b]);
        }
    }
    int main()
    {
        scanf("%d%d", &n, &m);
        for(int i = 0;i < n;i ++) scanf("%d", &w[i]);
        while((double)clock() / CLOCKS_PER_SEC < 0.8) sim();//决定退火次数
        printf("%.2lf", ans);
        return 0;
    }