关于模拟退火

随机数的生成

一、随机数

现有的计算机的运算过程都是确定性的,因此,仅凭借算法 来生成真正 不可预测、不可重复 的随机数列是不可能的。

然而在绝大部分情况下,我们都不需要如此强的随机性,而只需要所生成的数列在统计学上具有随机数列的种种特征(比如均匀分布、互相独立等等)。这样的数列即称为 伪随机数 序列。

OI/ICPC 中用到的随机算法,基本都只需要伪随机数。这是因为,这些算法往往是 通过引入随机数来把 概率 引入复杂度分析,从而 降低复杂度。这本质上依然只利用了随机数的 统计 特征。

二、随机数的实现

1. rand()

生成伪随机数的算法,较慢。需要 #include <cstdlib>

调用 rand() 函数会返回一个 [0, RAND_MAX] 中的随机 非负整数,其中 RAND_MAX 是标准库中的一个宏,在 Linux 系统下 RAND_MAX 等于 \(2^{31} - 1\)。可以用 取模 来限制所生成的数的大小。

使用 rand() 需要一个随机数 种子,可以使用 srand(seed) 函数来将随机种子更改为 seed,当然不初始化也是可以的。

同一程序使用 相同的 seed 两次运行,在同一机器、同一编译器下,随机出的结果将会是 相同 的。

有一个选择是使用 当前系统时间 来作为随机种子:srand(time(0))

  • 在 Windows 系统下 RAND_MAX\(2^{15} - 1\) ,当需要生成的数不小于 \(2^{15} - 1\) 时建议使用 (rand() << 15 | rand()) 来生成更大的随机数。

2. mt19937

是一个随机数生成器类,效用同 rand(),随机数的范围同 unsigned int 类型的取值范围。需要 #include<random>

优点:

随机数 质量高(一个表现为,出现循环的周期更长;其他方面也都至少不逊于 rand()),且 速度比 rand() 很多。

使用方法:

std::mt19937 myrand(seed)seed 可不填,不填 seed 则会使用默认随机种子。

mt19937 重载了 operator (),需要生成随机数时调用 myrand() 即可返回一个随机数。

  • 若需要将随机数范围扩大到 unsigned long long 类型的取值范围,可以使用 mt19937_64,使用方法同 mt19937

代码示例:

#include <ctime>
#include <iostream>
#include <random>

using namespace std;

int main() {
	mt19937 myrand(time(0));
	cout << myrand() << '\n';
	return 0;
}

3. random_device(生成真随机数)

random_device 是一个基于 硬件 的均匀分布随机数生成器,在熵池耗尽前 可以高速生成随机数。该类在 C++11 定义,需要 #include <random>

由于熵池耗尽后性能急剧下降,所以建议用此方法生成 mt19937 等伪随机数的 种子,而不是直接生成。

4. 规定随机数分布的范围与概率

不明觉厉

类名 分布概率 数据类型
uniform_int_distribution 等概率分布 整数
uniform_real_distribution 等概率分布 实数
bernoulli_distribution 伯努利分布 布尔
binomial_distribution 二项分布 整数
negative_binomial_distribution 负二项分布 整数
geometric_distribution 几何分布 整数
poisson_distribution 泊松分布 整数
exponential_distribution 指数分布 实数
gamma_distribution \(\gamma\) 分布 实数
weibull_distribution 威布尔分布 实数
extreme_value_distribution 极值分布 实数
normal_distribution 标准正态(高斯)分布 实数
lognormal_distribution 对数正态分布 实数
chi_squared_distribution \(\chi^2\) 分布 实数
cauchy_distribution 柯西分布 实数
fisher_f_distribution 费舍尔 F 分布 实数
student_t_distribution 学生 t 分布 实数
discrete_distribution 离散分布 整数
piecewise_constant_distribution 分布在常子区间上 实数
piecewise_linear_distribution 分布在定义的子区间上 实数

5. 代码

生成 \([l, r]\) 范围内的 \(n\) 个随机整 / 实数:

#include <cstdio>
#include <random>
using namespace std;
typedef double db;

random_device R; //定义生成种子的类
mt19937 gen(R()); //定义生成随机数的类

inline int dgrand(int l, int r) {
	uniform_int_distribution<> dis(l,r); //生成随机整数的范围
	return dis(gen);
}

inline db dbrand(db l, db r) {
	uniform_real_distribution<> dis(l,r); //生成随机实数的范围
	return dis(gen);
}

int main() {
	
	int n;
	db l, r;
	scanf("%d%lf%lf", &n, &l, &r);
	for (int i = 1; i <= n; i++) {
		printf("%d%c", dgrand(l, r), "\n "[(bool)(i % 10)]); //l和r自动向下取整了
	}
	putchar('\n');
	for (int i = 1; i <= n; i++) {
		printf("%.6lf%c", dbrand(l, r), "\n "[(bool)(i % 10)]);
	}
	
	return 0;
}

参考资料

模拟退火 (Simulated Annealing, SA)

一、介绍

模拟退火是一种玄学的经典的随机化全局搜索算法。

模拟退火适用的问题通常是一些求最优解的问题,换言之就是求一个 连续函数的最值

一般这种问题只需要求一个最优解,而到达这个最优解的路径并不重要;朴素的搜索需要遍历所有的解的可能,这些解构成的解空间又太大,没办法在时间限制内全部遍历搜索。

如果在函数的值域上朴素随机,找到这个最值点的概率是很小的。

由于函数具有连续的性质,我们可以简单的直接向更大或者更小的方向不断移动,但显然这样我们可能找到一个 极值而非最值 点。

科学家们发现,物理学上物体退火降温的微观过程也是粒子不断随机跳动的过程,但是物体冷却后粒子却总是能处于最低的能量状态,所以我们可以模拟退火这个物理过程来求函数的最值。

二、思路

我们引入一个初始温度 \(T_0\)、温度变化率 \(\Delta T\) 和终止温度 \(T_k\),其中 \(T_0\) 是一个较大的正数,\(\Delta T\) 是一个略小于 \(1\) 的正数,\(T_k\) 是一个略大于 \(0\) 的正数。

开始时温度 \(T = T_0\),每次将 \(T \leftarrow T \times \Delta T\),直到 \(T \leq T_k\) 结束算法,来模拟一个降温的过程。

假设我们当前选择解的位置是 \(x\)温度 \(T\) 代表了我们每次选的新解位置在 \(x\) 周围的跨度大小。具体来说,我们每次需要在一定范围内随机一个参数 \(\Delta x\),然后比较 \(f(x + \Delta x)\)\(f(x)\) 的大小。

令 $ f(x + \Delta x) - f(x) = \Delta E$。

如果 \(f(x + \Delta x)\)\(f(x)\) 更优,即 \(\Delta E < 0\),那么我们 直接接受更优的解

如果 \(\Delta E > 0\),我们 有一定概率接受新的解,这个概率是 \(\text{e}^{-\Delta E / T}\)。新的解越差,\(\Delta E\) 越大,这个概率越小;温度 \(T\) 越低,这个概率越小。

每次重复这个过程,直到温度趋于零(或者一个设定好的下限)。

形象的演示:

From Wikipedia

时间复杂度 O(玄学)。

三、调参

模拟退火时有三个参数,分别是初始温度 \(T_0\)、降温系数 \(\Delta T\)、终止温度 \(T_k\)

一般将 \(T_0\) 设置在 \(1000\) ~ \(5000\) 左右,\(\Delta T\) 设置在 \(0.95\) ~ \(0.999\) 之间, \(T_k\) 设置在 \(10^{-8}\) ~ \(10^{-15}\) 之间。

如果答案不是最优,有以下几种方法来优化:

  • 调大 \(\Delta T\)、调整 \(T_0\)\(T_k\)、多跑几遍模拟退火,从而增大得到最优解的概率;

  • 尝试更换随机数种子,或者 srand(rand())

  • 洗把脸再交一(亿)遍

四、Tricks

  • 一般情况下我们不会选取最终的解作为答案,而是在退火过程中不断记录一个全局最优的答案;

  • 一般情况下我们可以重复多次模拟退火的过程,来避免落入一个极值区间跨不出来的随机情况;

  • 在不会 TLE 的情况下尽量多地跑模拟退火:
    我们知道,有一个 clock() 函数,返回程序运行时间,那么这样即可:
    while ((double)clock()/CLOCKS_PER_SEC<MAX_TIME) SA();
    其中 MAX_TIME 是一个自定义的略小于题目时间限制的数;

  • 有时函数的峰很多,模拟退火难以跑出最优解,可以把整个值域分成几段,每段跑一遍模拟退火,然后再取全局最优解;

  • 有时为了使得到的解更有质量,会在模拟退火结束后,以当前温度在得到的解附近多次随机,以尝试得到更优的解。

五、例题(一)求函数最值

1. UVa10228 A Star not a Tree?

VJudge | Luogu | UVa

题意:给定二维平面上的 \(n\) 个点,求一个点使得该点到所有点距离之和最小(费马点)。

思路:赤果果的模拟退火模板题,具体请看代码注释。注意每组数据之间输出空行!

参考代码:

https://www.luogu.com.cn/paste/wdkwgtq9

2. [JSOI2004] 平衡点 / 吊打XXX

Luogu P1337

题意:给定平面上 \(n\) 个点的坐标和权值,求一个点使得该点到所有点的 距离与权值乘积 之和最小(相当于带权费马点)。

思路:与第 (1) 题类似,只是引入了权值。

参考代码:

https://www.luogu.com.cn/paste/t353wll8

3. [JSOI2016]炸弹攻击1

Luogu P5544

题意:在平面上有一些点和无重合面积的圆,求平面上的一个圆,使该圆覆盖最多的点,且与平面上的任意一个圆无重合面积。无重合面积是指不能有重合部分但可以相切。

思路:对所求的圆的圆心坐标进行退火,暴力计算每一个坐标所能覆盖的最多的点数。

参考代码:

https://www.luogu.com.cn/paste/6gypilfv

六、例题(二)求序列

一般用于数据范围较小,但爆搜会超时的情况。虽然不经典但好用

1. [TJOI2010]分金币

Luogu P3878

题意:多组数据。给定 \(n\) 个数 \(a_1, a_2,\dots,a_n\),将这些数分成大小为 \(\left\lfloor \dfrac{n}{2} \right\rfloor\)\(\left\lfloor \dfrac{n + 1}{2} \right\rfloor\) 的两组,使这两组数之和的差的绝对值最小。

数据范围: \(1 \leq T \leq 20,1 \leq n \leq 30,1 \leq v_i \leq 2^{30}\)

思路:分别从两组数,即 a[1 .. n/2]a[n/2+1 .. n] 中各随机抽取一个数并交换,比较交换前和交换后的两组数之和的差的绝对值。

参考代码:

https://www.luogu.com.cn/paste/cjqzk2tl

2. [HAOI2006]均分数据

Luogu P2503

题意:给定 \(n\) 个数 \(a_1,a_2,\dots,a_n\),将其分成 \(m\) 组,使得这 \(m\) 组数的均方差

\[\sigma = \sqrt{\frac1m \sum_{i = 1}^{m} (\bar x - x_i)^2}, \bar x = \frac1m \sum_{i = 1}^{m}x_i \]

最大。其中 \(x_i\) 为第 \(i\) 组数的和。

数据范围:\(m \leq n \leq 20\),且 \(2 \leq m \leq 6\)

思路:爆搜枚举每一种情况会超时,数学方法又不会,只好搞玄学的优雅的暴力——模拟退火。

首先考虑对于给定顺序的序列,如何使均方差最小?显然(意思是不会证)把当前数放到和最小的一组数中可以使答案最小。因此可以用模拟退火将序列顺序随机打乱(随机交换两个数即可,或者可以用 random_shuffle),求出最优答案。

参考代码:

https://www.luogu.com.cn/paste/ygvn01ph

3. Coloring

Luogu P3936

题意:

给定 \(n\)\(m\) 列的方格,用 \(m\) 种颜色将每个方格染上一种颜色。染色方案满足:

  • \(i\) 种颜色占 \(p_i\) 个方格,数据满足 \(\sum p_i = n \times m\)
  • 将每个格子上下左右与其颜色相同的格子视为位于同一个联通块内,定义不同联通块之间的方格边的条数为 \(q\),则该种方案是使 \(q\) 尽量小的一种方案。

由于可能存在不止一种方案,只需要输出任意一种方案即可。

约定:

本题采用 SPJ。对于每个测试点,将会设置阈值 \(w\),并保证存在构造使得 \(q \leq w\)

对于程序输出的答案,我们将会以以下方式计算得分:

\(q\) \(\text{score}\) \(q\) \(\text{score}\)
\(q \leq w\) \(10\) \(1.75w < q \leq 2w\) \(5\)
\(w < q \leq 1.1w\) \(9\) \(2w < q \leq 2.3w\) \(4\)
\(1.1w < q \leq 1.25w\) \(8\) \(2.3w < q \leq 2.6w\) \(3\)
\(1.25w < q \leq 1.5w\) \(7\) \(2.6w < q \leq 3w\) \(2\)
\(1.5w < q \leq 1.75w\) \(6\) \(3w < q \leq 3.5w\) \(1\)

\(q > 3.5w\),将以 Wrong Answer 处理。

数据范围:

对于 \(10\%\) 的数据,有 \(1\leq n,m\leq 3,c\leq 3\)

对于 \(30\%\) 的数据,有 \(1\leq n,m\leq 8,c\leq 6\)

对于 \(50\%\) 的数据,有 \(1\leq n,m\leq 15,c\leq 25\)

对于 \(100\%\) 的数据,有 \(1\leq n,m\leq 20,c\leq 50,p_i\leq 20\)

思路:

首先将所有方格按要求按顺序(也可以不按顺序)填色,计算初始的 \(q\)。然后在退火过程中随机打乱颜色顺序,计算此时的 \(q\)。不断退火得到尽量优的 \(q\)

参考代码:

https://www.luogu.com.cn/paste/df2utwz6

4. [SCOI2008]城堡

Luogu P2538

题意:

图中有 \(n\) 个城市(编号 \(0\) ~ \(n - 1\)),\(n\) 条双向道路,第 \(i\) 条道路连接了城市 \(i\) 和城市 \(r_i\),道路长为 \(d_i\)\(n\) 个城市中有 \(m\) 个城市中有城堡。定义一个城市的 \(\text{dis}\) 为距它最近的城堡到它的距离。你可以在 剩下没有城堡的 \(n-m\) 个城市中的不超过 \(k\) 个城市 建城堡,使得 \(\max\{\text{dis}\}\) 最小。

数据范围:\(2\leq n\leq 50,1\leq d_i\leq 10^6,0\leq m\leq n−k\)

思路:

显然多建城堡一定比少建城堡更优,所以只需考虑建 \(k\) 座城堡的情况。在没有城堡的 \(n - m\) 个城市中随机选 \(k\) 个建城堡,每次随机改变要建城堡的城市,进行模拟退火。

参考代码:

https://www.luogu.com.cn/paste/dffsztny

参考资料 / 延伸阅读:

  1. 再谈模拟退火 - RPChe_ 的博客 - 洛谷博客
  2. 浅谈玄学算法——模拟退火 - M_sea 的博客 - 洛谷博客
  3. 模拟退火 - OI Wiki
posted @ 2022-08-20 12:24  f2021ljh  阅读(124)  评论(0编辑  收藏  举报