模拟退火
模拟退火是常常用来解决最优化问题的算法 . 在 \(\mathrm{OI}\) 竞赛中广泛应用 .
虽然不知道正解是啥 , 但我给你退个几万遍火绝对不怂你 .
算法梗概
和 爬山算法 不同的是 , 爬山每次会找一个能上升的点 , 然后快速向那边爬 , 然后多随机几个初始点去爬 .
而 退火 就是 后继状态虽然不一定比当前优秀 , 但我仍然有几率向那边走 .
它是模拟热力学退火过程的一个神奇的算法 , 将我们 目标函数 作为 能量函数 最后使得能量越来越低 .
后继状态比当前要优秀 , 那么直接继承这个后继状态 .
高温的时候我们大概率选择不优秀的后继状态 , 低温的时候我们小概率选择不优秀的后继状态 , 因为越低温越稳定 .
实现过程 : 初始高温 \(\to\) 温度缓慢下降 \(\to\) 终止在低温 (这时能量函数达到最小,目标函数最小)
算法实现
假设我们当前是使得最后的答案最小 .
那么每次我们是否继承后继状态是分两种选择的 .
我们假设本来状态的答案是 \(bef\) , 随机改变状态后的答案是 \(res\) , 当前温度为 \(T\) .
- \(res < bef\) , 直接继承 \(res\) .
- \(res \ge bef\) , 我们以一定概率继承 就是 $$\displaystyle e ^ {\frac{-|res-bef|}{T}} \in (0,1]$$ 也就是温度越高 继承概率越大 , 改变的越小 继承概率越大 . 反之则反 .
然后最后经常会获得一个很优秀的最终状态 .
代码实现
\(ans\) 为全局的最优答案 .
\(Possible()\) 返回 \((0,1]\) 之间等概率随机的一个小数 . \(Init()\) 初始化 . \(Calc()\) 计算当前方案的答案 .
\(Change()\) 改变当前的方案 , \(Recover()\) 恢复之前的方案 .
有时候那个概率函数 \(exp\) 那里 , 容易写反 , 为了使得 \(e^x \le 1\) 那么我们强制使得 \(x \le 0\) 就行了 .
这个参数 \(DeltaT, eps, T\) 是随便选择的 , 一般的话需要调参找到最优的参数 .
const int lim = 1e4; const double eps = 1e-7; int ans = 1e9;
inline double Possible() { return rand() * 1.0 / RAND_MAX; }
inline void Simulate_Anneal() {
const double DeltaT = 0.99;
int res = Init();
for (double T = 1e6; T > eps; T *= DeltaT) {
int bef = res; Change(); res = Calc();
if (!(res < bef || exp(-fabs(res - bef) / T) > Possible())) res = bef, Recover();
ans = min(ans, res);
}
}
例题
BZOJ 3680 : 吊打XXX
有 \(n\) 个洞 , 每个洞坐标为 \((x_i, y_i)\) 用完全弹性的绳子下面挂一个质量为 \(m_i\) 的小球 .
然后所有绳子连向同一个绳结 . 忽略 摩擦 和 能量损失 (机械能守恒) .
球无限高 , 不会碰到地面 . \((1 \le n \le 1000)\)
一开始莫名奇妙地想到了机械能守恒 , 然后列动能和重力势能的方程 , 最后可以动态分析每个点的速度 ?
再来个动量守恒 ? 好吧 ... 那样就很毒瘤了 ...
直接考虑共点力的平衡 , 对所有力进行正交分解 , 然后再合成到一起 . 然后如果最后合成力越小越优秀 .
然后直接模拟退火 (爬山也行 , 因为单峰) 每次随机一个方向走 . 注意一开始跑多点 , 不然要找到最优解很慢 .
我们可以一开始走温度 \(T\) 那么长的路 这样就行了 .
挂一个 \(Calc()\) 看一下正交分解 ...
#define sqr(x) ((x) * (x))
inline double Calc(double x, double y) {
double sumx = 0.0, sumy = 0.0;
For (i, 1, n) {
double deltax = lis[i].x - x, deltay = lis[i].y - y;
double len = sqrt(sqr(deltax) + sqr(deltay));
if (fabs(len) <= eps) continue ;
sumx += lis[i].w * deltax / len;
sumy += lis[i].w * deltay / len;
}
return sqrt(sqr(sumx) + sqr(sumy));
}
BZOJ 2428: [HAOI2006]均分数据
有 \(n\) 个正整数 \(a_1 ... a_n\) . 现在将他们分成 \(m\) 组 , 使得每组数值和均方差最小 . 即
\[\displaystyle \sigma = \sqrt{\frac{\sum_{i=1}^n(x_i-\overline{x})^2}{n}}, \overline{x} = \frac{\sum _{i}^nx_i}{n} \]\(\sigma\) 为均方差 , \(\overline{x}\) 为各组数据和平均值 , \(x_i\) 为第 \(i\) 组数据的数值和 .
$ m \le n \le 20, 2 \le m \le 6$
这道题直接模拟退火就行了 , 似乎是模板题 ?
每次随机选择一个数 , 然后把他拿出来 , 然后贪心地放入当前权值最小的那一组中去 .
不然随便放的话就很找到的解就不优秀 . 基本上 \(100\) 次就可以退出最优解了 .