前言
为什么我去学了模拟退火呢?
因为 CSP2021T1 和 NOIP2021T3 都能用模拟退火得到很高的分数,但是我却只打了最简单的暴力。失去了很多分数。
本着补全科技树的想法,我稍微学了一下模拟退火。
下文参考了 OI Wiki。
注:未完工。
前置知识——随机数
众所周知计算机的随机数是伪随机数。
那么我们首先需要让他变得随机一些。
下面依次介绍几个 C++ 库中生成随机数的方法。
(因为不是非常常用,我怕我忘记,所以有这部分内容。)
rand()
需要 <cstdlib>
库。
用法:直接调用 rand()
返回一个 $[0,\text{RAND\_MAX}]$ 的随机数,Linux 下这个上界是 $2^{31}-1$。
随机种子:用 srand()
修改,否则使用默认种子。
优点:好记,常见。
缺点:慢,不是非常随机。
std::mt19937
需要 <random>
库。
用法:这是个类,可以用 std::mt19937 myrand()
来声明一个。重载了括号运算符,实际使用时和 rand()
一样,随机数范围同 unsigned int
范围。
优点:快,更强一些。
缺点:不太好记。
还有个 std::mt19937_64
,范围同 unsigned long long
。
简介
假设有一个很莫名其妙的函数,要求它的极值,模拟退火算法可以帮助我们找到答案。(但不一定是函数)
首先,假设我们有一个局部最优解。
假设我们随机到了另一个状态,它更优,我们肯定更新最优解。
如果它更劣呢?我们不能把它忽略,因为这样就丧失了找到整体最优解的机会;但也不能直接就跳过去,这样局部最优解就找不到了。
那么我们就以一定概率去接受这个新状态。
定义当前温度 $T$,有一个已知状态,通过已知状态随机得到一个新状态,两者差为 $\Delta E$($\Delta E \geq0$),那么发生状态转移(更新答案)的概率为: $$ P(\Delta E)= \begin{cases} 1&\text{新状态更优}\\ e^\frac{-\Delta E}{T}&\text{新状态更劣} \end{cases} $$ 解释一下:显然我们需要让差越大转移的概率越低,而且温度越低转移到劣解的概率越低。
实现
有三个参数 $T_0,d,T_{end}$。分别为初始温度(一个较大的数),降温系数(接近 $1$ 的数),终止温度(接近 $0$ 的正数)。
首先让当前温度 $T=T_0$,然后尝试转移一次,接着让 $T\leftarrow T\times d$,直到 $T<T_{end}$。得到最优解。
在找到最优解之后可以再用初始很低的温度再稍微跑一跑模拟退火,可能会找到更优解。
关于这三个参数,需要自己根据样例调整。(或者和暴力对拍)
小技巧——卡时
clock()
函数返回程序运行时间,于是可以这么写:
while ((double)clock() / CLOCKS_PER_SEC < 0.95)
SA();
后面的 0.95 是自定义的一个数,要小于时限。
这样可以多执行几遍模拟退火,增加准确性。
NOIP2021 方差
题面不说了。
这个题正解不是模拟退火,不过可以用模拟退火得到 $60+$ 分,而且非常简单,超值。
需要注意的是不能每次对差分数组随机一个排列,这样没意义,不满足模拟退火的下一个状态要从这一个状态转移来的要求。
所以直接随机选一个执行操作即可。
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <iostream>
#include <random>
using std::cout;
using std::endl;
const int maxn = 1e4 + 10;
typedef long long ll;
int a[maxn], n, b[maxn];
ll sumpow, sum;
ll calc()
{
sumpow = 0, sum = 0;
for (int i = 1; i <= n; i++)
sumpow += b[i] * b[i], sum += b[i];
return sumpow * n - sum * sum;
}
inline double rnd01() { return (double)rand() / RAND_MAX; }
inline void modify(int pos) { b[pos] = b[pos - 1] + b[pos + 1] - b[pos]; }
ll ans = 1e18;
std::mt19937 rng;
void SA()
{
double T0 = 100000, d = 0.99997, Tend = 0.00001;
for (int i = 1; i <= n; i++)
b[i] = a[i];
ll nowans = 1e18;
while (T0 > Tend)
{
int pos = rng() % (n - 2) + 2;
modify(pos);
ll res = calc();
ans = std::min(ans, res);
if (res <= nowans)
nowans = res;
else if (exp((double)(nowans - res) / T0) > rnd01())
nowans = res;
else
modify(pos);
T0 *= d;
}
}
int main()
{
scanf("%d", &n);
rng = std::mt19937(time(0));
srand(time(0));
for (int i = 1; i <= n; i++)
scanf("%d", &a[i]);
while ((double)clock() / CLOCKS_PER_SEC < 0.85)
SA();
cout << ans << endl;
}