模拟退火

模拟退火

模拟退火有什么用?

模拟退火是模拟物理上退火方法,通过N次迭代(退火),逼近函数的上的一个最值(最大或者最小值)。
比如逼近这个函数的最大值C点:(非常大的概率找到最优解)

什么是模拟退火?(基于物理模型)

模拟退火算法的思想借鉴于固体的退火原理,当固体的温度很高的时候,内能比较大,固体的内部粒子处于快速无序运动,当温度慢慢降低的过程中,固体的内能减小,粒子的慢慢趋于有序,最终,当固体处于常温时,内能达到最小,此时,粒子最为稳定

怎么做?(以求最小值为例)

温度(步长):初始温度 \(T_0\)
,终止温度 \(T_E\) ,衰减系数 \(T=T∗0.99\) ,其中,衰减系数在 \((0,1)\) 中,衰减系数越大,温度衰减越慢,找到最优解的概率越大
1、先随机找一点 \(x_0\) 作为当前点 ,不论是哪个点都可以,随机!(不超过定义域就行)
2、每次循环时(即每次降低温度),在所在步长氛围中随机选一个点(温度越大步长越大),\(f(新点)−f(旧点)=△E\)

情况1:\(△E<0\) ,则一定跳到新点
情况2:\(△E>0\) ,则以一定概率跳去新点,概率是 \(e−△E/T\)(越接近最优解,跳过去的概率最大;越不接近最优解,跳过去的概率越小)

实现过程

1、提出在保证数据传输的可靠前提下,可以明显降低数据传输的逻辑距离,达到降低网络整体能耗的效果

2、把信源节点通过中转节点把数据传送到多个目标节点的距离问题,抽象成n个点多边形找费马点位置的模型,使用模拟退火算法计算出费马点位置,

3、选出最优中转节点,建立节点移动分析模型,建立节点移动路由表

伪代码模板

#include <bits/stdc++.h>

#define rint register int
#define int long long
#define endl '\n'

using namespace std;

const double parameter = 0.5;
//可以固定一个,也可以随机一个,但是在 0 到 1 之间

namespace RAND
{
	mt19937 Rand(random_device{}());
	
	int rand_int()
	{
		return uniform_int_distribution<>(l, r)(Rand);
	}
	
	double rand_double() 
	{
		return uniform_real_distribution<>(l, r)(Rand);
	}
}

using namespace RAND;

int calc()
{
	......
}

void simulate_anneal()
{
	......
	for (double t = 1e5; t > 1e-5; t *= 0.99)
	{
		......
		double delta = calc() - x;
		if (exp(-delta / t) > parameter)
		{
			......
		}
	}
}

signed main()
{
	......
	
	int times = ;
	while (times--) simulate_anneal();
	//while(1.0 * clock() / CLOCKS_PER_SEC < num)  num表示此题时限
	//如果对于一个题无法确定时限,可使用这个
}

例题

AcWing3167. 星星还是树

题目传送门

#include <bits/stdc++.h>

#define rint register int
#define int long long
#define endl '\n'

#define x first
#define y second

using namespace std;

typedef pair<double, double> PDD;

const int N = 1e2 + 5;
const double inf = 1e8;
const double parameter = 0.5;

int n;
PDD q[N];
double ans = inf;

namespace RAND
{
	mt19937 Rand(random_device{}());
	
	int rand_int(int l, int r)
	{
		return uniform_int_distribution<>(l, r)(Rand);
	}
	
	double rand_double(int l, int r) 
	{
		return uniform_real_distribution<>(l, r)(Rand);
	}
}

using namespace RAND;

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 (rint i = 1; i <= n; i++)
	{
		res += get_dist(q[i], p);
	}
	ans = min(res, ans);
	return res;
}

void simulate_anneal()
{
	PDD cur(rand_double(0, 10000), rand_double(0, 10000));
	for (double t = 1e4; t > 1e-4; t *= 0.99)
	{
		PDD new_point(rand_double(cur.x - t, cur.x + t), rand_double(cur.y - t, cur.y + t));
		double delta = calc(new_point) - calc(cur);
	    // 如果新点得到的距离和小于原来点得到的距离和,dt < 0,-dt/t > 0,exp(-dt/t)
	    // 范围 > 1,原来点一定更新为新点
	    // 如果新点得到的距离和大于原来点得到的距离和,dt > 0,-dt/t < 0,exp(-dt/t)
	    // 范围(0, 1)中间,此时随缘更新原来的点为新点
	    // 模拟退火的退火就在与即便新点效果没有原来的佳,也有一定概率接受这个新点
		if (exp(-delta / t) > parameter)
		{
			cur = new_point;
		}
	}
}

signed main()
{
	cin >> n;
	
	for (rint i = 1; i <= n; i++)
	{
		cin >> q[i].x >> q[i].y;
	}
	
	int times = 1e2 + 5;
	while(times--) simulate_anneal();
	
	cout << (int)(ans + 0.5) << endl;
	
	return 0;
}

[JSOI2014] 保龄球

题目传送门

在本题中,对于每个轮次,有三种情况:全中,补中,失误。我们需要将打出的所有轮次的顺序重新排列,使得得分最高。

补中会使选手在下一轮中的第一次尝试的得分将会以双倍计入总分。失误的情况属于一般情况,不具有特殊性,所以不做处理。最重要的是全中的情况。全中会使选手在计算总分时,下一轮的得分将会被乘 2 计入总分,最需要特殊处理的是,当原来最后一轮次全中,我们在重新排列的时候,也需要最后一轮次是全中,因为这样子才会有奖励的轮次,需要进行的轮数和重排前所进行的轮数是一致的,才满足题意。

然后退几次火即可。

#include<bits/stdc++.h>

#define rint register int
#define int long long
#define endl '\n'

#define x first 
#define y second

using namespace std;

typedef pair<int, int> PII;

const int N = 1e2 + 5;
const double parameter = 0.5;

int n, ans;
PII a[N];

namespace RAND
{
	mt19937 Rand(random_device{}());
	
	int rand_int()
	{
		return uniform_int_distribution<>(0, 1e6)(Rand);
	}
	
	double rand_double() 
	{
		return uniform_real_distribution<>(0, 1e6)(Rand);
	}
}

using namespace RAND;

int calc()
{
    int res = 0;
    int m = n + (a[n].x == 10);
    for(int i = 1; i <= m; i++)
    {
        res += a[i].x + a[i].y;
        if(a[i].x == 10) 
		{
			res += a[i + 1].x + a[i + 1].y;
		}
        else if(a[i].x + a[i].y == 10) 
		{
			res += a[i + 1].x;
		}
    }
    ans = max(ans, res);
    return res;
}

void simulate_anneal()
{
    int m = n + (a[n].x == 10);
    for(double t = 1e4; t >= 1e-4; t *= 0.99)
    {
        int x = rand_int() % m + 1;
		int y = rand_int() % m + 1;
        int p = calc();
        swap(a[x], a[y]);
        if(m == n + (a[n].x == 10))
        {
            int q = calc(), delta = q - p;
            if(exp(delta / t) < parameter) 
			{
				swap(a[x], a[y]);
			}
        }
        else 
		{
			swap(a[x], a[y]);
		}
    }
}

signed main()
{
    cin >> n;
    
    for(rint i = 1; i <= n; i++)
    {
        cin >> a[i].x >> a[i].y;		
	}
    
    if(a[n].x == 10) 
	{
		cin >> a[n + 1].x >> a[n + 1].y;
	}

    int times = 1e2 + 5;
    while (times--) simulate_anneal();
        
    cout << ans << endl;
    
    return 0;
}

[HAOI2006] 均分数据

题目传送门

将每个数放入某一组中有一个贪心策略:每次将该数放和最小的组中

这样的策略不一定能构造出答案,但是如果每次模拟退火将序列随机化就一定可以构造出答案,另外构造出的序列交换两点变化不大,即函数具有一定的连续性,故可用模拟退火求解

#include<bits/stdc++.h>

#define rint register int
#define int long long
#define endl '\n'

using namespace std;

const int N = 3e1 + 5;
const int M = 1e1 + 5;
const double parameter = 0.5;

int n, m;
int a[N], s[M];
double x, ans = 1e9;

namespace RAND
{
	mt19937 Rand(random_device{}());
	
	int rand_int()
	{
		return uniform_int_distribution<>(0, 1e6)(Rand);
	}
	
	double rand_double() 
	{
		return uniform_real_distribution<>(0, 1e6)(Rand);
	}
}

using namespace RAND;

double get()
{
    memset(s, 0, sizeof s);
    for(rint i = 1; i <= n; i++)
    {
        int t = 1;
        for(rint j = 2; j <= m; j++)
        {
            if(s[j] < s[t]) 
			{
				t = j;		
			}	
		}
        s[t] += a[i];
    }
    double res = 0;
    for(rint i = 1; i <= m; i++)
    {
        res += (s[i] - x) * (s[i] - x);		
	}
    res /= m;
    res = sqrt(res);
    ans = min(ans, res);
    return res;
}

void simulate_anneal()
{
    random_shuffle(a + 1, a + n + 1);
    for(double t = 1e5; t >= 1e-6; t *= 0.9)
    {
        int p = rand_int() % n + 1;
		int q = rand_int() % n + 1;
        double x = get();
        swap(a[p], a[q]);
        double y = get(), delta = y - x;
        if(exp(-delta / t) < parameter)
		{
			swap(a[p], a[q]);
		} 
    }
}

signed main()
{
    cin >> n >> m;
    
    for(rint i = 1; i <= n; i++)
    {
        cin >> a[i];
        x += a[i];
    }
    x /= m;
    
    int times = 1e2 + 5;
    while (times--) simulate_anneal();

    printf("%.2lf\n", ans);
    
    return 0;
}

[NOIP2021] 方差

题目传送门

在我的另一篇文章 随机化贪心 中有提及这一题,并给出了一个可读性较低的代码,这里花十分钟重新整了一下。

事实证明,模拟退火可以卡过这道题。

#include <bits/stdc++.h>

#define rint register int
#define int long long
#define endl '\n'

using namespace std;

const int N = 1e4 + 5;
const int inf = 1e18;

int n, a[N], c[N];

int ans = inf;
vector<int> v1, v2, v3, v4, v5, v6;
double tim;

int calc() 
{
    int s = 0, w = 0;
    int p = 0;
    for (rint i = v1.size() - 1; i >= 0; i--) 
	{
        p += v1[i];
        s += p;
		w += p * p;
    }
    for (auto i : v2)
    {
        p += i;
        s += p;
		w += p * p;
    }
    w = w * n - s * s;
    ans = min(ans, w);
    return w;
}

namespace RAND
{
	mt19937 Rand(random_device{}());
	
	int rand_int()
	{
		return uniform_int_distribution<>(0, 1e8)(Rand);
	}
	
	double rand_double() 
	{
		return uniform_real_distribution<>(0, 1)(Rand);
	}
}

using namespace RAND;

void simulate_anneal()
{
    long long now = ans;
    v1 = v3, v2 = v4;
    int x, val;
    for (double t = 1e6; t > 1e-6; t *= 0.99)
	{
        if ((clock() - tim) * 1000 >= 980 * CLOCKS_PER_SEC) 
		{
            cout << ans << endl;
            exit(0);
        }
        x = rand_int() % (n - 1);
        v5 = v1;
		v6 = v2;
        if (x < (int)v1.size()) 
		{
            val = v1[x];
            v1.erase(v1.begin() + x);
            v2.insert(lower_bound(v2.begin(), v2.end(), val), val);
        } 
		else 
		{
            x -= v1.size();
            val = v2[x];
            v2.erase(v2.begin() + x);
            v1.insert(lower_bound(v1.begin(), v1.end(), val), val);
        }
        int delta = calc() - now;
        double parameter = rand_double();
        if (delta < 0 || exp(-delta / t) > parameter)
        {
            now += delta;			
		}
        else
        {
            v1 = v5;
			v2 = v6;			
		}
    }
}

signed main() 
{
    tim = clock();
    cin >> n;

    for (rint i = 1; i <= n; i++)
    {
		cin >> a[i];
		c[i] = a[i] - a[i - 1];
	}

    sort(c + 2, c + n + 1);

    for (rint i = 2; i <= n; i += 2)
    {
        v1.push_back(c[i]);		
	}

    for (rint i = 3; i <= n; i += 2)
    {
        v2.push_back(c[i]);		
	}

    v3 = v1;
	v4 = v2;
    calc();

    while (1) simulate_anneal();

    return 0;
}
posted @ 2022-07-22 17:19  PassName  阅读(162)  评论(0编辑  收藏  举报