参考:浅谈玄学算法——模拟退火
简介
模拟退火算法(Simulate Anneal,SA)是一种通用概率演算法,用来在一个大的搜寻空间内找寻命题的最优解。模拟退火是由S.Kirkpatrick,C.D.Gelatt和M.P.Vecchi在1983年所发明的。V.Černý在1985年也独立发明此演算法。模拟退火算法是解决TSP问题的有效方法之一。
模拟退火的出发点是基于物理中固体物质的退火过程与一般组合优化问题之间的相似性。模拟退火算法是一种通用的优化算法,其物理退火过程由加温过程、等温过程、冷却过程这三部分组成。
——百度百科
简单说,模拟退火是一种随机化算法。当一个问题的方案数量极大(甚至是无穷的)而且不是一个单峰函数时,我们常使用模拟退火求解。
它与爬山算法最大的不同是,在寻找到一个局部最优解时,赋予了它一个跳出去的概率,也就有更大的机会能找到全局最优解。
原理
模拟退火的原理也和金属退火的原理近似:将热力学的理论套用到统计学上,将搜寻空间内每一点想像成空气内的分子;分子的能量,就是它本身的动能;而搜寻空间内的每一点,也像空气分子一样带有“能量”,以表示该点对命题的合适程度。演算法先以搜寻空间内一个任意点作起始:每一步先选择一个“邻居”,然后再计算从现有位置到达“邻居”的概率。
——百度百科
简单来说,就是将金属加热到一定温度,保持足够时间,然后以适宜速度冷却。
那么对应到OI上,就是每次随机出一个新解,如果这个解更优,则接受它,否则以一个与温度和与最优解的差相关的概率接受它。
设这个新的解与最优解的差为 Δ E \Delta E ΔE ,温度为 T T T , k k k 为一个随机数,那么这个概率为 e Δ E k T e^{\frac{\Delta E}{kT}} ekTΔE
降温过程
模拟退火时有三个参数,分别是初始温度
T
0
T_0
T0 、降温系数
Δ
\Delta
Δ 、终止温度
T
k
T_k
Tk
其中,
T
0
T_0
T0 是一个比较大的数,
Δ
\Delta
Δ 是一个略小于
1
1
1 的正数,
T
k
T_k
Tk 是一个略大于
0
0
0 的正数
我们先让温度
T
=
T
0
T=T_0
T=T0,然后每次降温时
T
=
T
⋅
Δ
T=T\cdot \Delta
T=T⋅Δ ,直到
T
≤
T
k
T\leq T_k
T≤Tk 为止
大致过程如下:
可以看出,随着温度的降低,解逐渐稳定下来,并逐渐集中在最优解附近。
时间复杂度: O ( 玄学 ) O(\text{玄学}) O(玄学)
一般降温系数 Δ \Delta Δ 与 1 1 1 的差减少一个数量级, 耗时大约多 10 10 10 倍; T 0 T_0 T0 和 T k T_k Tk 变化一个数量级, 耗时不会变化很大
如何生成新解
- 坐标系内:随机生成一个点,或者生成一个向量
- 序列问题: r a n d o m _ s h u f f l e random\_shuffle random_shuffle 或者随机交换两个数
- 网格问题:可以看做二维序列,每次交换两个格子即可
注意事项
这里的 新解与最优解的差为
Δ
E
\Delta E
ΔE,对应代码里的
d
e
l
del
del
如果新解更优,则直接更新,否则以一定概率接受它,概率为 exp(-del/T)*RAND_MAX > rand()
要注意这里的
d
e
l
del
del 前有没有负号要看能不能跑出正解,括号内的必须是负值
若接受这个不优解,不能更新答案,而是在这个不优解的影响下继续降温
while(1.0*clock()/CLOCKS_PER_SEC < 0.85) sa();
可用来极限卡时间
例题一:P1337 [JSOI2004]平衡点 / 吊打XXX
题目大意:多个重物系在绳子上,绳子穿过桌面上的洞,绳子完全弹性
将绳子系在一起后,绳结X最终平衡于何处?
明显最后平衡时重力势能最小,即
∑
i
=
1
n
d
i
∗
w
i
\sum_{i=1}^{n}d_i*w_i
∑i=1ndi∗wi,
d
i
d_i
di 为平衡点到该点的距离
因为桌面上的绳子长度越大,下方绳子越短,势能越大。然后就可开始退火,随机生成新点
#include<bits/stdc++.h>
#define rint register int
#define deb(x) cerr<<#x<<" = "<<(x)<<'\n';
using namespace std;
typedef long long ll;
typedef pair <int,int> pii;
const int maxn = 1e3 + 5;
const double eps = 1e-15;
int n;
double ansx, ansy, ans;
struct node{
double x, y, w;
} a[maxn];
double cal(double x, double y){
double ret = 0;
for(int i=1; i<=n; i++)
ret += sqrt((a[i].x-x)*(a[i].x-x)+(a[i].y-y)*(a[i].y-y)) * a[i].w;
return ret;
}
void sa(){
double T = 6666, nowx = ansx, nowy = ansy, DT = 0.99;
while(T > eps){
double tx = nowx + (rand()*2-RAND_MAX) * T;
double ty = nowy + (rand()*2-RAND_MAX) * T;
double now = cal(tx, ty);
double del = now - ans;
if(del < 0){
ans = now;
ansx = tx, ansy = ty;
nowx = tx, nowy = ty;
} else if(exp(-del/T)*RAND_MAX > rand()){
nowx = tx, nowy = ty;
}
T = T * DT;
}
}
signed main() {
srand(time(0));
scanf("%d", &n);
for(int i=1; i<=n; i++){
scanf("%lf%lf%lf", &a[i].x, &a[i].y, &a[i].w);
ansx += a[i].x, ansy += a[i].y;
}
ansx /= n, ansy /= n;
ans = cal(ansx, ansy);
for(int i=1; i<10; i++) sa();
printf("%.3f %.3f\n", ansx, ansy);
}
例题二:HDU 5017 Ellipsoid
给定一个要满足的椭球的方程
a
x
2
+
b
y
2
+
c
z
2
+
d
y
z
+
e
x
z
+
f
x
y
=
1
ax^2+by^2+cz^2+dyz+exz+fxy=1
ax2+by2+cz2+dyz+exz+fxy=1
求球面上一个点到原点
(
0
,
0
,
0
)
(0,0,0)
(0,0,0) 的距离最小
则可以随机一个点 ( x , y ) (x,y) (x,y),然后求出 z z z,即可开始退火
#include<bits/stdc++.h>
#define rint register int
#define deb(x) cerr<<#x<<" = "<<(x)<<'\n';
using namespace std;
typedef long long ll;
typedef pair <int,int> pii;
const double eps = 1e-15;
double a, b, c, d, e, f;
double ansx, ansy, ansz, ans;
double cal(double x, double y, double z){
return sqrt(x*x + y*y + z*z);
}
double calz(double x, double y){
double A = c;
double B = d * y + e * x;
double C = a * x * x + b * y * y + f * x * y - 1.0;
double dlt = B * B - 4.0 * A * C;
if(dlt < 0) return 1e15;
double x1 = (-B + sqrt(dlt)) / (2.0 * A);
double x2 = (-B - sqrt(dlt)) / (2.0 * A);
if(cal(x, y, x1) < cal(x, y, x2)) return x1;
return x2;
}
void sa() {
double T = 66666, nowx = ansx, nowy = ansy, DT = 0.99;
while(T > eps) {
double tx = nowx + (rand()*2-RAND_MAX) * T;
double ty = nowy + (rand()*2-RAND_MAX) * T;
double tz = calz(tx, ty);
if(tz == 1e15) {T = T * DT; continue;}
double now = cal(tx, ty, tz);
double del = now - ans;
if(del < 0) {
ans = now;
ansx = tx, ansy = ty;
nowx = tx, nowy = ty;
} else if(exp(-del/T)*RAND_MAX > rand()) {
nowx = tx, nowy = ty;
}
T = T * DT;
}
}
signed main() {
srand(time(0));
while(~scanf("%lf%lf%lf%lf%lf%lf", &a, &b, &c, &d, &e, &f)) {
ansx = ansy = 0; ans = 1e15;
for(int i=1; i<5; i++) sa();
printf("%.5f\n", ans);
}
}
例题三:P2503 [HAOI2006]均分数据
n
n
n 个数分为
m
m
m 组,使得方差最小,注意不需要按顺序分组
序列上的随机,这里用交换两个位置上的数来产生新解
计算方差可以用
n
2
m
n^2m
n2m 的
d
p
dp
dp,也可以用
n
m
nm
nm 的贪心,每次将数放在最小的组里
#include<bits/stdc++.h>
#define rint register int
#define deb(x) cerr<<#x<<" = "<<(x)<<'\n';
using namespace std;
typedef long long ll;
typedef pair <int,int> pii;
const int maxn = 35;
const double eps = 1e-15;
int n, m, a[maxn];
double ans, ave, num[maxn];
double cal(){
memset(num, 0, sizeof(num));
for(int i=1; i<=n; i++){
int p = 1;
for(int j=1; j<=m; j++)
if(num[j] < num[p]) p = j;
num[p] += a[i];
}
double ret = 0;
for(int i=1; i<=m; i++) ret += (num[i] - ave) * (num[i] - ave);
return sqrt(ret / m);
}
void sa(){
random_shuffle(a+1, a+1+n);
double T = 66666, DT = 0.99;
while(T > eps){
int tx = rand() % n + 1;
int ty = rand() % n + 1;
while(ty == tx) ty = rand() % n + 1;
swap(a[tx], a[ty]);
double now = cal();
double del = now - ans;
if(del < 0){
ans = now;
} else if(exp(-del/T)*RAND_MAX > rand()){
} else swap(a[tx], a[ty]);
T = T * DT;
}
}
signed main() {
srand(time(0));
scanf("%d%d", &n, &m);
for(int i=1; i<=n; i++) scanf("%d", a+i), ave += a[i];
ave = 1.0 * ave / m; ans = 1e15;
for(int i=1; i<20; i++) sa();
printf("%.2f\n", ans);
}