模拟退火
模拟退火
模拟退火有什么用?
模拟退火是模拟物理上退火方法,通过N次迭代(退火),逼近函数的上的一个最值(最大或者最小值)。
比如逼近这个函数的最大值C点:(非常大的概率找到最优解)
什么是模拟退火?(基于物理模型)
模拟退火算法的思想借鉴于固体的退火原理,当固体的温度很高的时候,内能比较大,固体的内部粒子处于快速无序运动,当温度慢慢降低的过程中,固体的内能减小,粒子的慢慢趋于有序,最终,当固体处于常温时,内能达到最小,此时,粒子最为稳定。
怎么做?(以求最小值为例)
温度(步长):初始温度
,终止温度 ,衰减系数 ,其中,衰减系数在 中,衰减系数越大,温度衰减越慢,找到最优解的概率越大
1、先随机找一点 作为当前点 ,不论是哪个点都可以,随机!(不超过定义域就行)
2、每次循环时(即每次降低温度),在所在步长氛围中随机选一个点(温度越大步长越大),
情况1: ,则一定跳到新点
情况2: ,则以一定概率跳去新点,概率是 (越接近最优解,跳过去的概率最大;越不接近最优解,跳过去的概率越小)
实现过程
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;
}
本文作者:PassName
本文链接:https://www.cnblogs.com/spaceswalker/p/16506463.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步