模拟退火
概念:
-
温度(步长):
-
初始温度 \(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?
模拟退火裸题,也可以用三分套三分做
cpp
#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\) 时,我们就获得了一个答案。
如何随机一个序列,只需要随机交换两个数即可。
cpp
#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),取最大值即可
cpp
#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;
}