「筆記」模擬退火


寫在前面

感謝 caq 的傾情講解 qq_emoji: bx

模擬退火是個隨機化算法,正確性有一定保證,但如果你想我一樣臉黑的話......

實測模擬退火做多了 rp 會掉

正文

簡介

模擬退火是一種隨機化算法,當一個問題的方案數量極大(甚至是無窮的)而且不是一個單峰函數時,我們常使用模擬退火求解。- Oi-wiki

什么是退火?

退火是一種金屬熱處理工藝,指的是將金屬緩慢加熱到一定溫度,保持足夠時間,然后以適宜速度冷卻。目的是降低硬度,改善切削加工性;消除殘余應力,穩定尺寸,減少變形與裂紋傾向;細化晶粒,調整組織,消除組織缺陷。准確的說,退火是一種對材料的熱處理工藝,包括金屬材料、非金屬材料。而且新材料的退火目的也與傳統金屬退火存在異同。---百度百科

qq_emoji: fad

扯遠了。

這個算法就是在溫度不斷降低的過程中,不斷地從當前位置尋找別的位置進行計算,溫度越低,也就是它的動能越小時,位置就會變化的越小,最后逐漸停留在最優解(或者附近)

算法流程

每次隨機一個新的狀態,如果狀態更優就更新答案,否則以一定概率接受這個狀態。

Metropolis准則

以求最小值為例。

  • 如果 \(\Delta E < 0\),說明當前解更優,直接更新即可

  • 否則,如果

\[e^{\frac{-\Delta E}{T}} > \frac{\text{rand()}}{\text{RAND_MAX}} \]

就接受這個狀態。

  • 否則 直接跳過。

為什么?
第一步因為是最優解所以一定選擇更新答案
第二步后邊的是一個隨機值我們暫且不論。
考慮整個退火過程,
假設溫度 \(T\) 不變,新的解越劣,\(\Delta E\) 越大,左項的值越小,接受的概率也越小。
假設 \(\Delta E\) 不變,隨着溫度的下降,求解的范圍也趨於穩定,\(T\) 越小,左項得值也越小,接受的概率也越小

扔一張圖可能會更好理解:

聽上去很扯 ,但它還是有一定的正確性的。

SA 函數

通常降溫系數 \(d\) 是一個很接近 \(1\) 的數,終止溫度 \(T_0\) 是一個很接近 \(0\) 的數

這里給一個偽代碼:

const double lim = ... // 溫度最小值,通常為 1e-10 左右
const double d = ... // 變化系數,通常為 0.996 左右
void SA() {
    double T = ... // 初始溫度,通常為 2021 左右
    while(T > lim) {
        ... // 獲取一個隨機的位置
        now = calc(); // 計算當前位置的答案 
        del = now - ans; // 計算 變化量
        if(del < 0) { // 以最小值為例
            ans = now; // 更新答案
            ...  // 更新答案和中間量的狀態
        } else if(exp(-del/T) > (double)rand()/RAND_MAX) {
            ...  // 一定概率選擇當前當前狀態
        } 
        T *= d; // 降溫
    }
}

計算函數 calc

依據題目而定,這里不給出

一些技巧/思想

如果想要隨機一個無限大平面內的一個點,可以這樣:

double nowx = limx + ((rand() << 1) - RAND_MAX) * T;
double nowy = limy + ((rand() << 1) - RAND_MAX) * T;

其中 nowx,nowy 是我們隨機的位置, limx, limy 是我們一個中間狀態的位置(注意不是答案的位置),
后面的那一坨剛好對應着溫度越小變化越小的實際情況。


我們有時為了使得到的解更有質量,會在模擬退火結束后,以當前溫度在得到的解附近多次隨機狀態,嘗試得到更優的解(其過程與模擬退火相似)。


模擬退火是個隨機的算法,執行次數越多獲得的解越有可能更優,所以我們可以執行多遍 SA 函數。至於如何控制時間?

while((double)clock()/CLOCKS_PER_SEC < 0.90) SA();

上面這個代碼控制時間在 \(0.90s\) 左右,如果時間限制為 \(1s\),而每次 SA 函數執行時間略長時,就要小心可能會 \(\text{T}\) 掉了。


如果一個代碼不行,就考慮換個種子吧。

srand(...);

為了獲得更精確的解,也可以把 \(d\)\(T_0\) 調的更精准一點

const double d = 0.996 -> 0.99996;
const double lim = 1e-10 -> 1e-15;

還有,隨機亂搞一些 初溫,終溫,降溫系數 也是可以的。


隨機微調

對於序列分組這類問題,每次可以隨機兩個位置交換計算新解的貢獻。
但要注意如果沒有接受這個狀態時要把兩個位置在交換回去。


打亂排序

對於一些統計貢獻與序列順序無關的題可以考慮。

函數 random_shuffle() 可以在 \(O(n)\) 的時間內隨機打亂一個序列
包含在 ctime 中,使用方法和 sort 類似。
可以用下面那道 P2503 來練練手。

Tips

模擬退火通常用來解決一些數據范圍比較小,但對正解的計算不好算的問題。
通過隨機一些狀態來不斷逼近正解。
其計算過程通常是 簡單粗暴 的。
其正確性可能是 玄學 的。

這些例題中都有體現,注意體會。

例題

UVA10228 A Star not a Tree?

題目傳送

初始點的位置考慮選擇所有點的平均值(人類本質?)

按照上面說的每次隨機一個點即可。

如何計算一個點的貢獻?\(n\) 的數據范圍只有 \(100\),暴力計算即可。

看一下代碼可能會更好理解。

/*
Work by: Suzt_ilymics
Problem: 不知名屑題
Knowledge: 垃圾算法
Time: O(能過)
CAQ yyds !!!!
*/
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<ctime>
#define LL long long
#define orz cout<<"lkp AK IOI!"<<endl

using namespace std;
const int MAXN = 1e5+5;
const int INF = 1e9+7;
const int mod = 1e9+7;
const double d = 0.996;
const double lim = 1e-10;

int n;
double ax[MAXN], ay[MAXN];
double limx, limy; // 上一次選擇保留的位置 
double ansx, ansy; // 存答案的位置; 
double ans;

int read(){
    int s = 0, f = 0;
    char ch = getchar();
    while(!isdigit(ch))  f |= (ch == '-'), ch = getchar();
    while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0' , ch = getchar();
    return f ? -s : s;
}

double dis(double sx, double sy, double ex, double ey) {
    return sqrt((sx - ex) * (sx - ex) + (sy - ey) * (sy - ey));
}

double calc(double sx, double sy) {
    double sum = 0.0;
    for(int i = 1; i <= n; ++i) {
        sum += dis(sx, sy, ax[i], ay[i]);
    }
    return sum;
}

void SA() {
    double T = 2021; // 設置初始溫度 
    while(T > lim) { // 當沒降完溫繼續降溫 
        double nowx = limx + ((rand() << 1) - RAND_MAX) * T; // 獨特的隨機一個點的公式
        double nowy = limy + ((rand() << 1) - RAND_MAX) * T;
        double now = calc(nowx, nowy);//計算貢獻
        double del = now - ans; // 計算 改變量
        if(del < 0) {
            ansx = nowx, ansy = nowy;
            limx = nowx, limy = nowy;
            ans = now;
        } else if(exp(-del / T) > (double)rand() / RAND_MAX) { // 概率的選擇接受這個解 
            limx = nowx, limy = nowy;
        }
        T *= d; // 降溫 
    }
}

void work() {
    limx /= (1.0 * n); // 取一個平均值
    limy /= (1.0 * n);
    ansx = limx, ansy = limy;
    ans = calc(limx, limy);
    for(int i = 1; i <= 10; ++i) SA(); // 多執行幾遍 SA 函數
}

int main()
{
    int t = read();
    for(int i = 1; i < t; ++i) { // UVA 獨特的數據測試方式
        limx = limy = ansx = ansy = 0.0;
        ans = 0;
        n = read();
        for(int j = 1; j <= n; ++j) {
            scanf("%lf%lf", &ax[j], &ay[j]);
            limx += ax[j], limy += ay[j];
        }
        work();
        printf("%.0lf\n\n", ans);
        
    }
    n = read();
    for(int j = 1; j <= n; ++j) {
        scanf("%lf%lf", &ax[j], &ay[j]);
        limx += ax[j], limy += ay[j];
    }
    work();
    printf("%.0lf\n", ans);
    return 0;
}

P5544 [JSOI2016]炸彈攻擊1

題目傳送

肯定要先隨機一個點,考慮如何確定 \(R\)

隨機嗎?

注意隨機一個量的時候只能有一個參照量,也就是說這里不能同時隨機點的位置和爆炸半徑。
因為我們無法簡單的確定一個決策依據。

人類本質?

能炸多少炸多少啊!

所以暴力找到最大爆炸范圍統計貢獻就行啊。

/*
Work by: Suzt_ilymics
Problem: 不知名屑題
Knowledge: 垃圾算法
Time: O(能過)
*/
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<cstdlib>
#include<ctime>
#define LL long long
#define orz cout<<"lkp AK IOI!"<<endl

using namespace std;
const int MAXN = 1e5+5;
const int INF = 1e9+7;
const int mod = 1e9+7;
const double d = 0.996;
const double lim = 1e-10;

struct node {
    int x, y, r;
}a[MAXN];

struct node2 {
    int x, y;
}b[MAXN];

int n, m, R;
double limx, limy, ansx, ansy;
int ans;

int read(){
    int s = 0, f = 0;
    char ch = getchar();
    while(!isdigit(ch))  f |= (ch == '-'), ch = getchar();
    while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0' , ch = getchar();
    return f ? -s : s;
}

double GetR(double sx, double sy, int x) {
    return sqrt((sx - a[x].x) * (sx - a[x].x) + (sy - a[x].y) * (sy - a[x].y)) - a[x].r;
}

double Dis(double sx, double sy, int x) {
    return sqrt((sx - b[x].x) * (sx - b[x].x) + (sy - b[x].y) * (sy - b[x].y));
}

int calc(double sx, double sy) {
    double maxR = 1.0 * R;
    for(int i = 1; i <= n; ++i) {
        maxR = min(maxR, GetR(sx, sy, i));
    }
    int cnt = 0;
    for(int i = 1; i <= m; ++i) {
        if(Dis(sx, sy, i) <= maxR) cnt++;
    }
    return cnt;
}

void SA() {
    double T = 2021;
    while(T > lim) {
        double nowx = limx + ((rand() << 1) - RAND_MAX) * T;
        double nowy = limy + ((rand() << 1) - RAND_MAX) * T;
        int now = calc(nowx, nowy);
        int del = now - ans;
        if(del > 0) {
            ansx = nowx, ansy = nowy;
            limx = nowx, limy = nowy;
            ans = now;
        } else if(exp(-del/T) < (double) rand() / RAND_MAX) {
            limx = nowx, limy = nowy;
        }
        T *= d;
    }
}

void work() {
    ansx = ansy = limx = limy = 0.0;
    ans = calc(limx, limy);
    while((double)clock() / CLOCKS_PER_SEC < 0.70) SA(); // CLOCKS_PER_SEC
}

int main()
{
    n = read(), m = read(), R = read();
    for(int i = 1; i <= n; ++i) a[i].x = read(), a[i].y = read(), a[i].r = read();
    for(int i = 1; i <= m; ++i) b[i].x = read(), b[i].y = read();
    work();
    printf("%d", ans);
    return 0;
}

P2503 [HAOI2006]均分數據

題目傳送

原題面中的式子可能有點錯誤,應該把 \(m\) 組當成 \(m\) 個元素來計算方差

介紹一個函數 random_shuffle(),可以在 \(O(n)\) 的時間內,將一段區間隨機打亂。

考慮最簡單的想法,

每次隨機打亂,貪心的分組。

每次加進一個元素,將它放進當前 \(m\) 組中最小的那一組中。

然后計算貢獻。

多隨機幾遍,總有一遍我們會碰到最優解的。

/*
Work by: Suzt_ilymics
Problem: 不知名屑題
Knowledge: 垃圾算法
Time: O(能過)
*/
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<ctime>
#define LL long long
#define orz cout<<"lkp AK IOI!"<<endl

using namespace std;
const int MAXN = 1e5+5;
const int INF = 1e9+7;
const int mod = 1e9+7;
const double d = 0.996;
const double lim = 1e-10;

int n, m;
int a[22];
int b[7];
int sum;
double ave, ans = 10000000.0;

int read(){
    int s = 0, f = 0;
    char ch = getchar();
    while(!isdigit(ch))  f |= (ch == '-'), ch = getchar();
    while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0' , ch = getchar();
    return f ? -s : s;
}

double calc() {
    memset(b, false, sizeof b);
    for(int i = 1; i <= n; ++i) {
        int wz = 1;
        for(int j = 1; j <= m; ++j) if(b[wz] > b[j]) wz = j;
        b[wz] += a[i];
    }
    double Sum = 0;
    for(int i = 1; i <= m; ++i) {
        Sum += (ave - 1.0 * b[i]) * (ave - 1.0 * b[i]);
    }
    Sum /= (1.0 * m);
    return sqrt(Sum);
}

void SA() {
    double T = 2021;
    while(T > lim) {
        random_shuffle(a + 1, a + n + 1);
        double now = calc();
        ans = min(ans, now);
//        double del = now - ans;
////        cout<<now<<endl;
//        if(del < 0) {
//            ans = now;
//        } 
        T *= d;
    }
}

void work() {
    ave = 1.0 * sum / m;
    for(int i = 1; i <= 20; ++i) SA();
}

int main()
{
    n = read(), m = read();
    for(int i = 1; i <= n; ++i) a[i] = read(), sum += a[i];
    work();
    printf("%.2lf", ans);
    return 0;
}

P3878 [TJOI2010]分金幣

題目傳送

前一半分一組,后一半分一組。

然后每次隨機兩個位置交換一下計算貢獻。

nowx = rand() % n + 1; // 整數位置的隨機直接 % n + 1 即可

這里與板子略有不同

如果更優就更新,
否則考慮概率接受,
否則你要把元素再換回去!

計算函數應該很好想,依舊是暴力,如果還不會就看代碼吧。

/*
Work by: Suzt_ilymics
Problem: 不知名屑題
Knowledge: 垃圾算法
Time: O(能過)
*/
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<ctime>
#define int long long
#define LL long long
#define orz cout<<"lkp AK IOI!"<<endl

using namespace std;
const int MAXN = 1e5+5;
const int INF = 1e12+7;
const int mod = 1e9+7;
const double d = 0.996;
const double lim = 1e-10;

int t, n, ans = INF;
int a[40];

int read(){
    int s = 0, f = 0;
    char ch = getchar();
    while(!isdigit(ch))  f |= (ch == '-'), ch = getchar();
    while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0' , ch = getchar();
    return f ? -s : s;
}

int calc() {
    int sum1 = 0, sum2 = 0;
    for(int i = 1; i <= n / 2; ++i) sum1 += a[i];
    for(int i = n / 2 + 1; i <= n; ++i) sum2 += a[i];
    return abs(sum1 - sum2);
}

void SA() {
    double T = 2021;
    while(T > lim) {
        int nowx = rand() % n + 1;
        int nowy = rand() % n + 1;
        swap(a[nowx], a[nowy]);
        int now = calc();
        int del = now - ans;
        if(del < 0) {
            ans = now;
        } else if(exp(-del/T) > (double)rand() / RAND_MAX) {
        } else swap(a[nowx], a[nowy]);
        T *= d;
    }
}

void work() {
    for(int i = 1; i <= 100; ++i) SA();
}

signed main()
{
    t = read();
    while(t--) {
        n = read();
        ans = INF;
        for(int i = 1; i <= n; ++i) a[i] = read();
        random_shuffle(a + 1, a + n + 1);
        work();
        printf("%lld\n", ans);
    }
    return 0;
}

P3936 Coloring

題目傳送

不要害怕,加油,奧利給!

選擇按順序染色作為初始狀態。
然后開始微調。

如何更快的計算貢獻?
在原來的基礎上減去兩個位置原來位置的貢獻,然后加上交換后的貢獻。

推薦參數:

d = 0.9999lim = 1e-15T = 500srand(20041011)

這樣的話分數就可以在 \(97pts\) 以上了

有個疑問?
我計算 \(del\) 的時候是當前值與交換前的值作差,而不是和最優值作差。可前面幾道題都是和最優值作差,可這道題這樣做卻死活過不去。有沒有大佬可以解釋一下啊,也可以到我這個帖子看一看。

/*
Work by: Suzt_ilymics
Problem: 不知名屑題
Knowledge: 垃圾算法
Time: O(能過)
*/
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<queue>
#include<ctime>
#define LL long long
#define orz cout<<"lkp AK IOI!"<<endl

using namespace std;
const int MAXN = 1e5+5;
const int INF = 1e9+7;
const int mod = 1e9+7;
const double d = 0.99998;
const double lim = 1e-15;
int dx[] = {1, -1, 0, 0};
int dy[] = {0, 0, 1, -1};

int n, m, c, ans, now_;
int p[55], cnt[55], top = 1;
int col[22][22], acol[22][22];

int read(){
    int s = 0, f = 0;
    char ch = getchar();
    while(!isdigit(ch))  f |= (ch == '-'), ch = getchar();
    while(isdigit(ch)) s = (s << 1) + (s << 3) + ch - '0' , ch = getchar();
    return f ? -s : s;
}

bool check(int x, int y) { return x < 1 || y < 1 || x > n || y > m; }

int Dev(int sx, int sy) {
    int Cnt = 0;
    for(int i = 0; i < 4; ++i) {
        int x = sx + dx[i], y = sy + dy[i]; // 找相鄰的四個格子計算貢獻 
        if(check(x, y)) continue;
        if(col[sx][sy] != col[x][y]) Cnt++; 
    }
    return Cnt;
}

//int Dev(int x, int y) { return (col[x][y] != col[x-1][y] && x!=1) + (col[x][y] != col[x][y-1] && y!=1) + (col[x][y] != col[x+1][y] && x != n) + (col[x][y] != col[x][y+1] && y != m); }

int calc(int sx, int sy, int ex, int ey) {
    int res = 0;
//    orz;
    res = res - Dev(sx, sy) - Dev(ex, ey); // 減去原來的貢獻 
    swap(col[sx][sy], col[ex][ey]);
    res = res + Dev(sx, sy) + Dev(ex, ey); // 加上現在的貢獻 
    return res;
}

void SA() {
//    orz;
//    for(int i = 1; i <= n; ++i) for(int j = 1; j <= m; ++j) col[i][j] = acol[i][j]; 
//    now_ = ans;
    double T = 500;
    while(T > lim) {
        int nowsx = rand() % n + 1;
        int nowsy = rand() % m + 1;
        int nowex = rand() % n + 1;
        int nowey = rand() % m + 1;
//        int pre = Calc();
//        swap(col[nowsx][nowsy], col[nowex][nowey]); // 先交換 
//        cout<<calc(nowsx, nowsy, nowex, nowey)<<"\n";
        int now = now_ + calc(nowsx, nowsy, nowex, nowey); // 再計算 
//        cout<<ans<<" "<<now_<<" "<<now<<"\n";
        int del = now - now_; // now_ 存的是當前 col 矩陣的答案 
//        int del = now - ans; // ans 存的是最優解 
        if(del < 0) { // 更新這個解
            ans = now; 
            now_ = now;
            for(int i = 1; i <= n; ++i) for(int j = 1; j <= m; ++j) acol[i][j] = col[i][j];  
        } else if(exp(-del/T) > (double) rand() / RAND_MAX) {
            now_ = now; // 一定概率接受當前這個狀態 
        } else {
            swap(col[nowsx][nowsy], col[nowex][nowey]); // 交換回去 
        }
        T *= d;
    }
}

void work() {
//    for(int i = 1; i <= n; ++i) {
//        for(int j = 1; j <= m; ++j) {
//            ans += Dev(i, j);
//        }
//    }
//    ans /= 2;
    for(int i = 2; i <= n; ++i) 
        for(int j = 1;j <= m; ++j) 
            if(col[i][j] != col[i - 1][j]) ans++;
    for(int i = 1; i <= n; ++i) 
        for(int j = 2;j <= m; ++j) 
            if(col[i][j] != col[i][j - 1]) ans++;
    now_ = ans;
//    cout<<ans<<" "<<now_<<endl;
//    cout<<ans<<endl;
//    for(int i = 1; i <= 30; ++i) SA();
    while((double)clock() / CLOCKS_PER_SEC < 3.90) SA();//, cout<<ans<<" "<<now_<<endl;
}

int main()
{
    srand(20041011);
    n = read(), m = read(), c = read();
    for(int i = 1; i <= c; ++i) p[i] = read();
    for(int i = 1; i <= n; ++i) {
        for(int j = 1; j <= m; ++j) {
            if(cnt[top] == p[top]) ++top;
            acol[i][j] = col[i][j] = top;
            ++cnt[top];
        }
    }
    work();
//    printf("%d\n", ans);
    for(int i = 1; i <= n; ++i) {
        for(int j = 1; j <= m; ++j) {
            printf("%d ", acol[i][j]);
        }puts("");
    }
    return 0;
}


/*
int dev(int sx, int sy, int ex, int ey) {
    int cnt = 0;
    for(int i = 0; i < 4; ++i) {
        int x = sx + dx[i], y = sy + dy[i]; // 與起點相鄰的點 
        if(check(x, y)) continue;
        if(col[ex][ey] != col[x][y]) cnt++; 
    }
    for(int i = 0; i < 4; ++i) {
        int x = ex + dx[i], y = ey + dy[i]; // 與終點相鄰的點 
        if(check(x, y)) continue;
        if(col[sx][sy] != col[x][y]) cnt++; 
    }
//    cout<<"cnt1 :"<< cnt<<" ";
    return cnt;
} 

int Calc() {
    int cnt = 0;
    for(int i = 2; i <= n; ++i) 
        for(int j = 1;j <= m; ++j) 
            if(col[i][j] != col[i - 1][j]) cnt++;
    for(int i = 1; i <= n; ++i) 
        for(int j = 2;j <= m; ++j) 
            if(col[i][j] != col[i][j - 1]) cnt++;
    return cnt;
}
*/

幾個練習題

P2210 Haywire

P2538 [SCOI2008]城堡

SP4587 FENCE3 - Electric Fences


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM