题解:P2538 [SCOI2008] 城堡
Phi_Quadrant · · 题解
模拟退火写法
前言:
重温曾经的黑题,当今的紫题。由于该题的官方知识点是动态规划 DP,搜索,但对本蒟蒻来说太难,于是咱们就从随机化算法之模拟退火来入手。
蒟蒻的第一篇题解不好请见谅。
前往我的博客食用效果更佳。
题目大意
给定
算法思路
从爬山算法说起
爬山算法是一种简单的局部搜索算法:从随机解出发,每次尝试微调(做一些微小的扰动),若新解更优则接受。但它的缺陷是容易陷入局部最优,无法跳出当前区域去寻找全局更优解。
::::info[就像我学这个算法的时候老师所说的:] ::::
如下图(最优解为 绿色箭头,而爬山算法可能找到的最优解为 红色箭头。
(图片来源于 OI-Wiki)
模拟退火:以概率跳出局部最优
模拟退火受金属退火过程启发,通过引入“温度”参数控制搜索过程:
::::info[什么是退火?(选自 百度百科)] 退火是一种金属热处理工艺,指的是将金属缓慢加热到一定温度,保持足够时间,然后以适宜速度冷却。目的是降低硬度,改善切削加工性;消除残余应力,稳定尺寸,减少变形与裂纹倾向;细化晶粒,调整组织,消除组织缺陷。准确的说,退火是一种对材料的热处理工艺,包括金属材料、非金属材料。而且新材料的退火目的也与传统金属退火存在异同。 ::::
-
借 OI-Wiki 的一句话来概括就是:
如果新状态的解更优则修改答案,否则以一定概率接受新状态。
其核心在于Metropolis准则:若新解更差,以概率
不过此处需要介绍一个概念,或者说是戏称。
玄学调参
模拟退火的效果很大程度上取决于参数的选择,这通常需要多次尝试,因此也被戏称为“玄 学调参”。
关键参数包括:初始温度、降温速率 delta (我这里用的是 alpha )、终止温度 T_end,以及每次循环的次数。
经验所得的降温系数:0.97 或 0.997。
本题应用以及主要思路
- 计算所有无城堡城市到最近城堡的最大距离,即
\max_{c \in b} \min_{s \in a} \text{dist}(s, c) 。 - 模拟退火。
代码实现
#include<bits/stdc++.h>
using namespace std;
int mp[105][105];
int n,m,k;
int r[105],vis[105];
vector<int>a,b;
// 计算当前方案下的最大最小距离
int check(){
int ans=0;
for(int i=0;i<b.size();i++){
int mi=1e9;
for(int j=0;j<a.size();j++){
mi=min(mi,mp[a[j]][b[i]]);
}
ans=max(ans,mi);
}
return ans;
}
//祖传自定义随机数
int rnd(int x){
return ((1ull*rand()*rand()+rand())^rand())%x;
}
int SA(){
double T=1e9,alpha=0.997;//祖传模拟退火系数
int now=check();
if(k==0) return now;
if(m+k==n) return 0;
while(T>1e-4){
int x=rand()%k+m,y=rand()%(n-m-k);
swap(a[x],b[y]);
int nxt=check();
if(nxt<now || nxt>=now && rnd(10000000)<10000000*exp(-(nxt-now)/T)){//概率P
now=nxt;
}else{
swap(a[x],b[y]);
}
T*=alpha;//降温
}
return now;
}
int main(){
cin>>n>>m>>k;
for(int i=1;i<=n;i++){
cin>>r[i];
}
memset(mp,0x3f,sizeof(mp));
for(int i=1;i<=n;i++){
int d;
cin>>d;
r[i]+=1;
mp[i][r[i]]=mp[r[i]][i]=min(mp[i][r[i]],d);
mp[i][i]=0;
}
//此处用dij复杂度更优,但Floyd更好写(
for(int x=1;x<=n;x++){
for(int i=1;i<=n;i++){
for(int j=1;j<=n;j++){
mp[i][j]=min(mp[i][j],mp[i][x]+mp[x][j]);
}
}
}
for(int i=1;i<=m;i++){
int x;cin>>x;
a.push_back(x+1);
vis[x+1]=1;
}
for(int i=1;i<=n;i++){
if(!vis[i]){
if(a.size()<m+k) a.push_back(i);
else b.push_back(i);
}
}
clock_t sta=clock();
int ans=SA();
while((clock()-sta)*1.0/CLOCKS_PER_SEC<0.75){
ans=min(ans,SA());
}
cout<<ans;
return 0;
}
复杂度分析
- Floyd :
O(n^3) - 模拟退火: 包不会超时的:
(clock()-sta)*1.0/CLOCKS_PER_SEC<0.75。
不过模拟退火仅限于,题目实在超出你的能力范围,且模拟退火能拿到分时才慎用。