模擬退火詳解&P1433題解
前排提示:LZ是個菜比,有可能有講的不對的地方,請在評論區指出qwq
0.基本思想
模擬退火其實沒有那麼高大上。說白了就是初始化一個“溫度”。每次隨機亂選一個方案,如果比以前的方案優那麼就要,否則就以一定的概率要或者不要。當前方案越狗屎就越不想要,“溫度”越低越不想要。然後把溫度降低一些,反覆迴圈,直到溫度為0為止。
1.照本宣科 實現
Fuck CCF(小聲
呃,就以 臭名昭著 著名的TSP問題舉例子吧。
什麼?你不知道TSP?這個就是->點我。
其實正解是搜尋,但是\(O(n!)\)的時間複雜度實在傷不起(除了像本題一樣\(n\le15\)),所以考慮模擬退火。
首先,初始化一個初始”溫度“。越高越好,但是過高會讓程式變慢,至於為什麼以後再說
const double T0=1e5/*初始溫度*/,T_end=1e-4/*結束溫度(由於非常接近0可以看作0)*/;
void SA(){
double T=T0;//當前溫度
while(T>T_end){//對應”反覆迴圈,直到溫度為0為止。“這句話
}
}
然後胡亂生成一個解:
double calc(){//意思是查詢當前解的代價,具體到問題裡就是按照當前順序訪問要走多遠 double ret=0.0; for(int i=1;i<=n;i++){ ret+=g[ans[i-1]][ans[i]];//g陣列的意思是從一個點到另一個點要走多少 } return ret; } int random_disp(int l,int r){//意思是在區間[l,r]內隨機生成一個數 srand(time(NULL)); static std::mt19937 random_engine(rand()); if(l>r)swap(l,r); uniform_int_distribution<int> u(l,r); return u(random_engine); } const double T0=1e5/*初始溫度*/,T_end=1e-4/*結束溫度(由於非常接近0可以看作0)*/; void SA(){ double T=T0;//當前溫度 while(T>T_end){//對應”反覆迴圈,直到溫度為0為止。“這句話 int u=random_disp(1,n),v=random_disp(1,n); swap(ans[u],ans[v]); double new_sol=calc();//隨機交換兩個數,就相當於亂生成一個 //ans陣列的意義是訪問的順序 } }
判斷是否要這個解:
inline bool CBP(double x){//Choose by probability. //以概率x返回 true或者false if(x>=1.0)return true; if(x<=0.0)return false; srand(time(NULL)); static std::mt19937 random_engine(rand()); uniform_real_distribution<double> u(0.0,1.0); return u(random_engine)<=x; } double calc(){//意思是查詢當前解的代價,具體到問題裡就是按照當前順序訪問要走多遠 double ret=0.0; for(int i=1;i<=n;i++){ ret+=g[ans[i-1]][ans[i]];//g陣列的意思是從一個點到另一個點要走多少 } return ret; } int random_disp(int l,int r){//意思是在區間[l,r]內隨機生成一個數 srand(time(NULL)); static std::mt19937 random_engine(rand()); if(l>r)swap(l,r); uniform_int_distribution<int> u(l,r); return u(random_engine); } const double T0=1e5/*初始溫度*/,T_end=1e-4/*結束溫度(由於非常接近0可以看作0)*/; void SA(){ double T=T0;//當前溫度 while(T>T_end){//對應”反覆迴圈,直到溫度為0為止。“這句話 int u=random_disp(1,n),v=random_disp(1,n); swap(ans[u],ans[v]); double new_sol=calc();//隨機交換兩個數,就相當於亂生成一個 //ans陣列的意義是訪問的順序 if(new_sol<old_sol){//如果撞到狗屎運,隨機亂搞一個都比以前的解好 old_sol=new_sol;//那麼更新 }else if(CBP(exp(double(old_sol-new_sol)/T))){//否則以概率決定是否更新 old_sol=new_sol; }else{ swap(ans[u],ans[v]);//換回來,交換兩次等於沒換 } } }
等等,exp(double(old_sol-new_sol)/T)
是什麼意思?
這個我當初也蒙了半天(我太蔡了),儘量講的明白一點
先把它翻譯成數學語言:
\[e^{\frac{\Delta f}{T}} \]
再翻譯成人話:
\(e\) (是個常數,大約是2.7) 的 (以前解 - 當前解 )除以當前溫度次方
(以前解 - 當前解 ),也就是\(\Delta f\),一定是個負數,為什麼看看程式碼就知道了。
那麼,\(\Delta f\)越小(也就是絕對值越大),也就是當前解越狗屎,\(\frac{\Delta f}{T}\)就越小。當\(T\)越小,也就是溫度越小,\(\frac{\Delta f}{T}\)的絕對值也就越大,\(\frac{\Delta f}{T}\)也就越小。\(\frac{\Delta f}{T}\)越小,\(e^{\frac{\Delta f}{T}}\)也就越小(但一定大於0),正好對應了”當前方案越狗屎就越不想要,“溫度”越低越不想要。“這句話。
^通讀三遍再往下看
降低溫度並記錄遇到的最優解:
inline bool CBP(double x){//Choose by probability.
//以概率x返回 true或者false
if(x>=1.0)return true;
if(x<=0.0)return false;
srand(time(NULL));
static std::mt19937 random_engine(rand());
uniform_real_distribution<double> u(0.0,1.0);
return u(random_engine)<=x;
}
double calc(){//意思是查詢當前解的代價,具體到問題裡就是按照當前順序訪問要走多遠
double ret=0.0;
for(int i=1;i<=n;i++){
ret+=g[ans[i-1]][ans[i]];//g陣列的意思是從一個點到另一個點要走多少
}
return ret;
}
int random_disp(int l,int r){//意思是在區間[l,r]內隨機生成一個數
srand(time(NULL));
static std::mt19937 random_engine(rand());
if(l>r)swap(l,r);
uniform_int_distribution<int> u(l,r);
return u(random_engine);
}
const double T0=1e5/*初始溫度*/,T_end=1e-4/*結束溫度(由於非常接近0可以看作0)*/;
void SA(){
double T=T0;//當前溫度
while(T>T_end){//對應”反覆迴圈,直到溫度為0為止。“這句話
int u=random_disp(1,n),v=random_disp(1,n);
swap(ans[u],ans[v]);
double new_sol=calc();//隨機交換兩個數,就相當於亂生成一個
//ans陣列的意義是訪問的順序
if(new_sol<old_sol){//如果撞到狗屎運,隨機亂搞一個都比以前的解好
old_sol=new_sol;//那麼更新
}else if(CBP(exp(double(old_sol-new_sol)/T))){//否則以概率決定是否更新
old_sol=new_sol;
}else{
swap(ans[u],ans[v]);//換回來,交換兩次等於沒換
}
ans_val=min(ans_val,old_sol);
ans_val=min(ans_val,new_sol);//記錄最優解
T*=0.997;//緩緩降低
}
}
然後,不停迴圈,直到溫度為0為止。
Code:
#include <bits/stdc++.h>
using namespace std;
#define MAXN 20
int n,ans[MAXN],st=clock();
double g[MAXN][MAXN],x[MAXN],y[MAXN],ans_val=1e10;
double euc_dis(double x1,double y1,double x2,double y2){
return sqrt((x1-x2)*(x1-x2)+(y1-y2)*(y1-y2));
}
inline bool CBP(double x){//Choose by probability
if(x>=1.0)return true;
if(x<=0.0)return false;
srand(time(NULL));
static std::mt19937 random_engine(rand());
uniform_real_distribution<double> u(0.0,1.0);
return u(random_engine)<=x;
}
int random_disp(int l,int r){
srand(time(NULL));
static std::mt19937 random_engine(rand());
if(l>r)swap(l,r);
uniform_int_distribution<int> u(l,r);
return u(random_engine);
}
double calc(){
double ret=0.0;
for(int i=1;i<=n;i++){
ret+=g[ans[i-1]][ans[i]];
}
return ret;
}
const double T0=1e5,T_end=1e-4,DT=0.997;
void SA(){
double T=T0,old_sol=calc();
while(T>T_end){
int u=random_disp(1,n),v=random_disp(1,n);
swap(ans[u],ans[v]);
double new_sol=calc();
if(new_sol<old_sol){
old_sol=new_sol;
}else if(CBP(exp(double(old_sol-new_sol)/T))){
old_sol=new_sol;
}else{
swap(ans[u],ans[v]);
}
ans_val=min(ans_val,old_sol);
ans_val=min(ans_val,new_sol);
T*=DT;
}
}
int main(){
srand(time(NULL));
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%lf %lf",x+i,y+i);
}
for(int i=0;i<=n;i++){
for(int j=0;j<=n;j++){
g[i][j]=euc_dis(x[i],y[i],x[j],y[j]);
}
}
for(int i=1;i<=n;i++){
ans[i]=i;
}
while(clock()-st<0.95*CLOCKS_PER_SEC){//只要還沒超時就不停退火
SA();
}
printf("%.2lf\n",ans_val);
return 0;
}
以上程式碼能夠ACP1433,也就是例題。
3.一些注意事項
- 由於模擬退火是個概率演算法,所以除非你想不出正解最好不要用。
- 由於模擬退火是個概率演算法,所以最好多跑幾遍。
- 由於模擬退火是個概率演算法,所以要仔細調整幾個引數——初始溫度、結束溫度、變化率。
- 由於模擬退火是個概率演算法,所以時間複雜度是\(O(玄學)\)。初始溫度越高,溫度變化率越接近1,跑得越慢,也越精確。
- 由於模擬退火是個概率演算法,所以LZ想不出來怎麼繼續隊形了qwq。
- 由於模擬退火是個概率演算法,所以能給LZ點一個贊嗎qwq