模拟退火

模拟退火是常常用来解决最优化问题的算法 . 在 \(\mathrm{OI}\) 竞赛中广泛应用 .

虽然不知道正解是啥 , 但我给你退个几万遍火绝对不怂你 .

算法梗概

爬山算法 不同的是 , 爬山每次会找一个能上升的点 , 然后快速向那边爬 , 然后多随机几个初始点去爬 .

退火 就是 后继状态虽然不一定比当前优秀 , 但我仍然有几率向那边走 .

它是模拟热力学退火过程的一个神奇的算法 , 将我们 目标函数 作为 能量函数 最后使得能量越来越低 .

后继状态比当前要优秀 , 那么直接继承这个后继状态 .

高温的时候我们大概率选择不优秀的后继状态 , 低温的时候我们小概率选择不优秀的后继状态 , 因为越低温越稳定 .

实现过程 : 初始高温 \(\to\) 温度缓慢下降 \(\to\) 终止在低温 (这时能量函数达到最小,目标函数最小)

算法实现

假设我们当前是使得最后的答案最小 .

那么每次我们是否继承后继状态是分两种选择的 .

我们假设本来状态的答案是 \(bef\) , 随机改变状态后的答案是 \(res\) , 当前温度为 \(T\) .

  1. \(res < bef\) , 直接继承 \(res\) .
  2. \(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\) 次就可以退出最优解了 .

posted @ 2018-05-18 20:13  zjp_shadow  阅读(472)  评论(3编辑  收藏  举报