【知识】模拟退火 & 爬山法
模拟退火
概念:
-
温度(步长):
-
初始温度 \(T\)
-
终止温度
-
衰减系数 $ 0 \sim 1$
-
-
随机选择一个点:
\(f(新点) - f(当前点) = \Delta E\)
- \(\Delta E < 0\) 跳到新点
- \(\Delta E>0\) 以一定概率跳过去,概率为 \(e^{- \frac{\Delta E}{T}}\)
如何退火(降温)?
模拟退火时我们有三个参数:初始温度 \(T_0\),降温系数 \(d\),终止温度 \(T_k\)。其中 \(T_0\) 是一个比较大的数,\(d\) 是一个非常接近 \(1\) 但是小于 \(1\) 的数,\(T_k\) 是一个接近 \(0\) 的正数。
首先让温度 \(T=T_0\),然后按照上述步骤进行一次转移尝试,再让 \(T=d\cdot T\)。当 \(T<T_k\) 时模拟退火过程结束,当前最优解即为最终的最优解。
注意为了使得解更为精确,我们通常不直接取当前解作为答案,而是在退火过程中维护遇到的所有解的最优值。
过程如下图:
技巧:
卡时:while ((double)clock()/CLOCKS_PER_SEC < MAX_TIME) simulateAnneal();
这里的 MAX_TIME
是一个自定义的略小于时限的数(单位:s)。
题型:
-
A Star not a Tree?
模拟退火裸题,也可以用三分套三分做
#include <iostream> #include <cstring> #include <algorithm> #include <cmath> #include <ctime> #define x first #define y second using namespace std; typedef pair<double, double> PDD; const int N = 110; int n; PDD q[N]; double ans = 1e8; double rand(double l, double r) { return (double)rand() / RAND_MAX * (r - l) + l; } double get_dist(PDD a, PDD b) { double dx = a.x - b.x; double dy = a.y - b.y; return sqrt(dx * dx + dy * dy); } double calc(PDD p) { double res = 0; for (int i = 0; i < n; i ++ ) res += get_dist(p, q[i]); ans = min(ans, res); return res; } void simulate_anneal() { PDD cur(rand(0, 10000), rand(0, 10000)); for (double t = 1e4; t > 1e-4; t *= 0.9) { PDD np(rand(cur.x - t, cur.x + t), rand(cur.y - t, cur.y + t)); double dt = calc(np) - calc(cur); if (exp(-dt / t) > rand(0, 1)) cur = np; } } int main() { scanf("%d", &n); for (int i = 0; i < n; i ++ ) scanf("%lf%lf", &q[i].x, &q[i].y); for (int i = 0; i < 100; i ++ ) simulate_anneal(); printf("%.0lf\n", ans); return 0; }
-
P4044 [AHOI2014/JSOI2014] 保龄球
对于每个轮次,有三种情况:全中,补中,失误。我们需要将打出的所有轮次的顺序重新排列,使得得分最高。
其中,补中会使选手在下一轮中的第一次尝试的得分将会以双倍计入总分。失误的情况属于一般情况,不具有特殊性,所以不做处理。最重要的是全中的情况。全中会使选手在计算总分时,下一轮的得分将会被乘 \(2\) 计入总分,最需要 特殊处理 的是,当原来最后一轮次全中,我们在重新排列的时候,也需要最后一轮次是全中,因为这样子才会有奖励的轮次,需要进行的轮数和重排前所进行的轮数是一致的,才满足题意。
本题目中,我们用温度 \(T\) ,表示答案更新范围,当 \(T \rightarrow 0\),也就是达到终止温度 \(T_E\) 时,我们就获得了一个答案。
如何随机一个序列,只需要随机交换两个数即可。
#include <iostream> #include <cstring> #include <algorithm> #include <cmath> #include <ctime> #define x first #define y second using namespace std; typedef pair<int, int> PII; const int N = 55; int n, m; PII q[N]; int ans; int calc() { int res = 0; for (int i = 0; i < m; i ++ ) { res += q[i].x + q[i].y; if (i < n) { if (q[i].x == 10) res += q[i + 1].x + q[i + 1].y; else if (q[i].x + q[i].y == 10) res += q[i + 1].x; } } ans = max(ans, res); return res; } void simulate_anneal() { for (double t = 1e4; t > 1e-4; t *= 0.99) { int a = rand() % m, b = rand() % m; int x = calc(); swap(q[a], q[b]); if (n + (q[n - 1].x == 10) == m) { int y = calc(); int delta = y - x; if (exp(delta / t) < (double)rand() / RAND_MAX) swap(q[a], q[b]); } else swap(q[a], q[b]); } } int main() { cin >> n; for (int i = 0; i < n; i ++ ) cin >> q[i].x >> q[i].y; if (q[n - 1].x == 10) m = n + 1, cin >> q[n].x >> q[n].y; else m = n; for (int i = 0; i < 100; i ++ ) simulate_anneal(); cout << ans << endl; return 0; }
-
P2503 [HAOI2006] 均分数据
已知 \(n\) 个正整数 \(a_1,a_2 ... a_n\) 。将它们分成 \(m\) 组,使得方差最小。
\[\sigma = \sqrt{\frac 1m \sum\limits_{i=1}^m(\overline x - x_i)^2},\overline x = \frac 1m \sum\limits_{i=1}^m x_i \]其中 \(\sigma\) 为均方差,\(\bar{x}\) 为各组数据和的平均值。
原式:
\[\sigma=\sqrt{\sum_{i=1}^n\ (x_i-\bar{x})^2 \over n}\quad\bar{x}={\sum_{i=1}^n\ x_i \over n} \]化简:
\[n\sigma^2=\sum_{i=1}^n\ (x_i-\bar{x})^2 \]拆开:
\[n\sigma^2=\sum_{i=1}^n x_i^2-2\bar{x}\sum_{i=1}^{n}x_i + \sum_{i=1}^{n}\bar{x}^2 \]\(\because -2\bar{x}\sum_{i=1}^{n}x_i + \sum_{i=1}^{n}\bar{x}^2\) 为定值
也可以推测出当 \(\sum_{i=1}^{n}x_i\) 为定值,每个 \(x\) 尽量接近时,\(\sum_{i=1}^n x_i^2\) 最大。
于是我们可以将数组
random_shuffle
若干次,每次贪心的取值,使每个 x 尽量相等(把新加进来的数,加给最小 x),取最大值即可。#include <iostream> #include <cstring> #include <algorithm> #include <cmath> using namespace std; const int N = 25, M = 10; int n, m; int w[N], s[M]; double ans = 1e8; double 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 simulate_anneal() { random_shuffle(w, w + n); for (double t = 1e6; t > 1e-6; t *= 0.95) { 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() { cin >> n >> m; for (int i = 0; i < n; i ++ ) cin >> w[i]; for (int i = 0; i < 100; i ++ ) simulate_anneal(); printf("%.2lf\n", ans); return 0; }
爬山法:
爬山算法每次在当前找到的最优方案 \(x\) 附近寻找一个新方案。如果这个新的解 \(x'\) 更优,那么转移到 \(x'\),否则不变。
只能解决单峰函数问题,如果解决单峰问题可能会陷入局部最优解。
-
P4035 [JSOI2008] 球形空间产生器
题目大意: 给你 \(n\) 个点坐标,要你求出圆心
题解: 随机化,可以随机一个点当圆心,然后和每个点比较,求出平均距离 \(r\),如果到这个点的距离大于 \(r\),说明离这个点远了,就给圆心施加一个向这个点的力;若小于 \(r\) ,说明近了,就施加一个远离这个点的力。所有点比较完后,把假设的圆心按合力方向移动一个距离,距离和当前温度有关。时间越久,温度越低
#include <iostream> #include <cstring> #include <algorithm> #include <cmath> using namespace std; const int N = 15; int n; double d[N][N]; double ans[N], dist[N], delta[N]; void calc() { double avg = 0; for (int i = 0; i < n + 1; i ++ ) { dist[i] = delta[i] = 0; for (int j = 0; j < n; j ++ ) dist[i] += (d[i][j] - ans[j]) * (d[i][j] - ans[j]); dist[i] = sqrt(dist[i]); avg += dist[i] / (n + 1); } for (int i = 0; i < n + 1; i ++ ) for (int j = 0; j < n; j ++ ) delta[j] += (dist[i] - avg) * (d[i][j] - ans[j]) / avg; } int main() { scanf("%d", &n); for (int i = 0; i < n + 1; i ++ ) for (int j = 0; j < n; j ++ ) { scanf("%lf", &d[i][j]); ans[j] += d[i][j] / (n + 1); } for (double t = 1e4; t > 1e-6; t *= 0.99995) { calc(); for (int i = 0; i < n; i ++ ) ans[i] += delta[i] * t; } for (int i = 0; i < n; i ++ ) printf("%.3lf ", abs(ans[i])); return 0; }
很容易想到的是,为了尽可能获取优秀的答案,我们可以多次爬山。方法有修改初始状态/修改降温参数/修改初始温度等,然后开一个全局最优解记录答案。每次爬山结束之后,更新全局最优解。