【知识】模拟退火 & 爬山法

模拟退火

概念:

  1. 温度(步长):

    • 初始温度 T

    • 终止温度

    • 衰减系数 01

  2. 随机选择一个点:

    f()f()=ΔE

    • ΔE<0 跳到新点
    • ΔE>0 以一定概率跳过去,概率为 eΔET

    如何退火(降温)?

    模拟退火时我们有三个参数:初始温度 T0,降温系数 d,终止温度 Tk。其中 T0 是一个比较大的数,d 是一个非常接近 1 但是小于 1 的数,Tk 是一个接近 0 的正数。

    首先让温度 T=T0,然后按照上述步骤进行一次转移尝试,再让 T=dT。当 T<Tk 时模拟退火过程结束,当前最优解即为最终的最优解。

    注意为了使得解更为精确,我们通常不直接取当前解作为答案,而是在退火过程中维护遇到的所有解的最优值。

    过程如下图:

技巧:

卡时: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 ,表示答案更新范围,当 T0,也就是达到终止温度 TE 时,我们就获得了一个答案。

    如何随机一个序列,只需要随机交换两个数即可。

    #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 个正整数 a1,a2...an 。将它们分成 m 组,使得方差最小。

    σ=1mi=1m(xxi)2,x=1mi=1mxi

    其中 σ 为均方差,x¯ 为各组数据和的平均值。

    原式:

    σ=i=1n (xix¯)2nx¯=i=1n xin

    化简:

    nσ2=i=1n (xix¯)2

    拆开:

    nσ2=i=1nxi22x¯i=1nxi+i=1nx¯2

    2x¯i=1nxi+i=1nx¯2 为定值

    也可以推测出当 i=1nxi 为定值,每个 x 尽量接近时,i=1nxi2 最大。

    于是我们可以将数组 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;
    }
    

很容易想到的是,为了尽可能获取优秀的答案,我们可以多次爬山。方法有修改初始状态/修改降温参数/修改初始温度等,然后开一个全局最优解记录答案。每次爬山结束之后,更新全局最优解。

posted @   Star_F  阅读(66)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!
点击右上角即可分享
微信分享提示