1. 程式人生 > 實用技巧 >模擬退火詳解&P1433題解

模擬退火詳解&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.一些注意事項

  1. 由於模擬退火是個概率演算法,所以除非你想不出正解最好不要用。
  2. 由於模擬退火是個概率演算法,所以最好多跑幾遍。
  3. 由於模擬退火是個概率演算法,所以要仔細調整幾個引數——初始溫度、結束溫度、變化率。
  4. 由於模擬退火是個概率演算法,所以時間複雜度是\(O(玄學)\)。初始溫度越高,溫度變化率越接近1,跑得越慢,也越精確。
  5. 由於模擬退火是個概率演算法,所以LZ想不出來怎麼繼續隊形了qwq。
  6. 由於模擬退火是個概率演算法,所以能給LZ點一個贊嗎qwq

完結撒花~

完結撒CCF~