模拟退火学习笔记
虽然说考前不应该碰这些随机化算法,容易影响思考,但是还是写一写吧,对于一些问题还是很好用的。
概念
什么是模拟退火。一句话解释,我们从一个旧状态随机出一个新状态,要从旧状态转移到新状态,如果新状态小于旧状态那么就接受,否则以一定概率接受这个新状态。
注意这里的接受并不是指更新答案,是指保留当前状态。
感觉这个解释很好理解啊!我们直接来看例题吧。
应用——计算几何
P1337 [JSOI2004] 平衡点 / 吊打XXX
这道题题面描述写的比较阴间,大概意思就是给定你 个点,每个点有一个质量 ,然后你要找一个点 使得 最小。 其中 表示 和 之间的距离。 这你就可以直接退火了。
模拟退火你要考虑清楚两个问题,第一个是初始状态,第二个是计算代价。
那么这道题的初始状态是什么?其实初始状态你想设啥设啥,但是你为了让答案更加准确,需要设定一个尽量优的解。所以我们令初始状态是给定的坐标的平均数。
至于这个计算代价,这道题直接模拟一下就好了。
提供一个我觉得还不错的模板,后面变形要用。
#include<bits/stdc++.h> using namespace std; #define temp 1e5 #define cold 0.996 const int N=1e5+10; int n; double ans,ansx,ansy; struct node{ int x,y,w; }a[N]; double cost(double x,double y)//计算代价 { double energy=0.0; for(int i=1;i<=n;i++) { energy+=sqrt((x-a[i].x)*(x-a[i].x)+(y-a[i].y)*(y-a[i].y))*a[i].w; } return energy; } void SA() { double t=temp;//初始温度 while(t>1e-18)//退火 { double tmpx=ansx+(rand()+rand()-RAND_MAX)*t,tmpy=ansy+(rand()+rand()-RAND_MAX)*t; double tmp=cost(tmpx,tmpy); double d=tmp-ans; if(d<0.0)ans=tmp,ansx=tmpx,ansy=tmpy;//如果更优直接接受 else if(exp(-d/t)*RAND_MAX>rand())ansx=tmpx,ansy=tmpy;//否则以一定概率接受 t*=cold;//降温 } } void solve()//多次退火增加正确性 { ans=cost(ansx,ansy); SA(); SA(); SA(); SA(); SA(); SA(); } int main() { srand(time(0)); cin>>n; for(int i=1;i<=n;i++) { cin>>a[i].x; cin>>a[i].y; cin>>a[i].w; ansx+=a[i].x; ansy+=a[i].y; } ansx/=n; ansy/=n;//初始状态 solve(); printf("%.3lf %.3lf\n",ansx,ansy); return 0; }
应用——方差/平均数
P2503 [HAOI2006]均分数据
不知道为什么,但这种分组题真的很适合退火。
首先老套路,初始状态,这题你可以一开先跑一个假贪心,把这个数组扫一变,每个数放到当前和最小的那个组里。然后如何计算代价,实际上对于每一个状态跑一遍假贪心就可以了。
注意这道题跟上面那题的区别,上面那个题状态是一个坐标,那么这个题的状态是什么呢?我们考虑这个题暴力是怎么写的,是不是枚举全排列然后算。那么模拟退火时我们可以类似这样,每次随机出来两个下标,然后交换这两个下标。
#include<bits/stdc++.h> using namespace std; #define cold 0.95 #define temp 3000 const int N=50; int n; int m; int a[N]; int tmp[N]; double ans=1e9; double sum; double calc()//假贪心计算代价 { int w,mn; double summ=0; memset(tmp,0,sizeof(tmp)); for(int i=1;i<=n;i++) { mn=1e9; for(int j=1;j<=m;j++) { if(tmp[j]<mn) { mn=tmp[j]; w=j; } } tmp[w]+=a[i]; } for(int i=1;i<=m;i++) { summ+=(sum-tmp[i])*(sum-tmp[i]); } return sqrt(summ/m); } void SA() { double t=temp; while(t>1e-16) { int x=rand()%n+1; int y=rand()%n+1; swap(a[x],a[y]);//随机两个下标交换 double cnt=calc();//初始状态,这个其实可以放到下面去的 if(cnt<ans)ans=cnt;//接受 else if(exp((cnt-ans)/t)<double(rand())/RAND_MAX)swap(a[x],a[y]);//随机接受 t*=cold; } } int main() { srand(time(0)); cin>>n>>m; for(int i=1;i<=n;i++) { cin>>a[i]; sum+=a[i]; } sum/=m; while(clock()<CLOCKS_PER_SEC*0.9) SA();//算是模拟退火中一个比较重要的应用,注意有些题卡不了时就别卡了 printf("%.2f",ans); return 0; }
P3878 [TJOI2010]分金币
这道题又是一个分组的题,那模拟退火去吧!
老套路,初始状态是什么,我们直接对原数组对半砍,作为初始代价。转移状态?跟上面那题一样,每次随机两个下标交换。计算代价?按照题目模拟即可。
#include<bits/stdc++.h> using namespace std; #define int long long #define cold 0.981 #define temp 3000 const int N=50; int n; int a[N]; int ans; int calc()//计算代价 { int mid=(1+n)>>1; int sum1=0; int sum2=0; for(int i=1;i<=mid;i++) sum1+=a[i]; for(int i=mid+1;i<=n;i++) sum2+=a[i]; return abs(sum1-sum2); } void SA() { double t=temp; while(t>1e-16) { int x=rand()%n+1; int y=rand()%n+1; swap(a[x],a[y]);//随机两个下标 int cnt=calc(); if(cnt<ans)ans=cnt; else if(exp((ans-cnt)/t)<double(rand())/RAND_MAX)swap(a[x],a[y]); t*=cold; } } signed main() { srand(time(0)); int T; cin>>T; while(T--) { cin>>n; for(int i=1;i<=n;i++) cin>>a[i]; ans=1e9; for(int i=1;i<=100;i++) SA(); cout<<ans<<endl; } return 0; }
P2210 Haywire
初始状态是什么?可以对于最开始的这个排列即 求一下。
如何转移?随机两个下标交换。
计算代价?直接对于每一头牛算一下他和他三个朋友的位置差即可。但这样会重复算两次所以最后答案还要除 。
#include<bits/stdc++.h> using namespace std; #define cold 0.996 #define temp 1e5 const int N=50; int n; int a[N][N]; int ans; int pos[N]; int calc()//计算代价 { int ret=0; for(int i=1;i<=n;i++) { for(int j=1;j<=3;j++) { ret+=abs(pos[i]-pos[a[i][j]]); } } return ret; } void SA() { double t=temp; while(t>1e-16) { int x=rand()%n+1; int y=rand()%n+1; swap(pos[x],pos[y]);//交换位置 int cnt=calc(); if(cnt<ans)ans=cnt; else if(exp((ans-cnt)/t)<double(rand())/RAND_MAX)swap(pos[x],pos[y]); t*=cold; } } void solve() { SA(); SA(); SA(); SA(); SA(); SA(); SA(); cout<<ans/2<<endl;//答案要除2 } int main() { srand(time(0)); cin>>n; for(int i=1;i<=n;i++) { cin>>a[i][1]; cin>>a[i][2]; cin>>a[i][3]; pos[i]=i;//记录一下位置 } ans=calc(); solve(); return 0; }
P7962 [NOIP2021] 方差
模拟退火典中典应用。要不是这道题我为什么会来学模拟退火。
预警:这道题的数据不算特别水,退火能拿到较高的分数但想要通过需要一些玄学操作和性质的发现。
这道题首先那道题有一个显然的事实,题目给的这个操作的本质就时交换两个差分数组,因为你考虑 就是 。
那么我们就可以写出一个最基础的退火。
#include<bits/stdc++.h> using namespace std; #define int long long #define cold 0.94 #define temp 1000 const int N=1e5+10; int n,m; int sum[N]; int a[N]; int ans=1e9; int calc()//计算代价 { int cnt=0; int ret=0; for(int i=2;i<=n;i++) { a[i]=a[i-1]+sum[i];//差分数组的前缀和就是原数组 } for(int i=1;i<=n; i++) { cnt+=a[i]; } for(int i=1;i<=n;i++) { ret+=(a[i]*n-cnt)*(a[i]*n-cnt); } return ret/n; } void SA() { double t=temp; while(t>1e-16) { int x=rand()%(n-1)+2; int y=rand()%(n-1)+2; swap(sum[x],sum[y]);//交换差分数组 int cnt=calc(); if(cnt<ans)ans=cnt; else if(exp((ans-cnt)/t)<double(rand())/RAND_MAX)swap(sum[x],sum[y]); t*=cold; } } int cmp(int a,int b) { return a>b; } signed main() { srand(time(0)); cin>>n; for(int i=1;i<=n;i++) { cin>>a[i]; sum[i]=a[i]-a[i-1]; } while(clock()<CLOCKS_PER_SEC*0.95) SA(); cout<<ans<<endl; return 0; }
然后你就需要稍微厉害一点的找性质能力了,我们模拟一下样例发现差分数组时单谷的,那我们大胆对于所有的最优解答案都是单谷的。具体如何证明我也不大会,题解里说了。
所以我们一开始先将差分数组对半砍,前半段从大到小排序,后半段从小到大排序构成一个单谷函数,这样找答案的时候会快很多。
#include<bits/stdc++.h> using namespace std; #define int long long #define cold 0.94 #define temp 1000 const int N=1e5+10; int n,m; int sum[N]; int a[N]; int ans=1e9; int calc() { int cnt=0; int ret=0; for(int i=2;i<=n;i++) { a[i]=a[i-1]+sum[i]; } for(int i=1;i<=n; i++) { cnt+=a[i]; } for(int i=1;i<=n;i++) { ret+=(a[i]*n-cnt)*(a[i]*n-cnt); } return ret/n; } void SA() { double t=temp; while(t>1e-16) { int x=rand()%(n-1)+2; int y=rand()%(n-1)+2; swap(sum[x],sum[y]); int cnt=calc(); if(cnt<ans)ans=cnt; else if(exp((ans-cnt)/t)<double(rand())/RAND_MAX)swap(sum[x],sum[y]); t*=cold; } } int cmp(int a,int b) { return a>b; } signed main() { srand(time(0)); cin>>n; for(int i=1;i<=n;i++) { cin>>a[i]; sum[i]=a[i]-a[i-1]; } sort(sum+2,sum+1+n/2,cmp); sort(sum+n/2+1,sum+n+1); while(clock()<CLOCKS_PER_SEC*0.95) SA(); cout<<ans<<endl; return 0; }
本文作者:zplqwq
本文链接:https://www.cnblogs.com/zplqwq/p/16816069.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步