1. 程式人生 > 其它 >sscms/siteserver 站點首頁-公司簡介呼叫

sscms/siteserver 站點首頁-公司簡介呼叫

技術標籤:ACM--雜項

模擬退火

1.演算法描述

1.1 概述

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

它與爬山演算法最大的不同是,在尋找到一個區域性最優解時,賦予了它一個跳出去的概率,也就有更大的機會能找到全域性最優解。

在 OI 領域,對應的,每次隨機出一個新解,如果這個解更優,則接受它,否則以一個與溫度和與最優解的差相關的概率接受它。

以下圖片來自:https://oi-wiki.org/misc/simulated-annealing/

img

1.2 相關引數

初始溫度: T 0 T_0

T0

結束溫度: T s T_s Ts

降溫係數: Δ t \Delta t Δt。這樣每次溫度就是上次的溫度乘上 Δ t \Delta t Δt

能量差: Δ E = f ( t n e w ) − f ( t n o w ) \Delta E=f(t_{new})-f(t_{now}) ΔE=f(tnew)f(tnow),即新點的能量減去當前的能量(能量也就是函式值)

接受概率: P ( Δ E ) = e − Δ E t P(\Delta E)=e^{\frac{-\Delta E}{t}} P(ΔE)=etΔE,t為當前溫度,這樣保證當能量差小於0時,概率P是大於1的,也就是必然接受,當能量差大於0時,能量差越大越不容易接受,t越大越容易接受。針對具體問題為:

  1. 如果是求凹函式的最值,那麼判斷條件寫為:
if (exp(-dt / t) > rand(0, 1)) cur = new;  // 如果概率大於(0 ~ 1),那麼就跳到新的點
else continue;  // 否則不跳
  1. 如果是求凸函式的最值,那麼判斷條件寫為:
if (exp(dt / t) > rand(0, 1)) cur = new;  // 如果概率大於(0 ~ 1),那麼就跳到新的點
else continue;  // 否則不跳

1.3 技巧

1.由於較為玄學,所以需要多跑幾次模擬退火:

for (int i = 0; i < 100; i ++ ) simulate_anneal(
);

2.更改隨機種子

3.卡時間,例如小於0.8秒就一直跑模擬退火,充分利用測評時間:

while ((double)clock() / CLOCKS_PER_SEC < 0.8) simulate_anneal();

4.超時說明降溫係數太大了,降溫過程太慢,方法:可以把 Δ t \Delta t Δt 從0.999改為0.99

5.錯誤可以是因為降溫太快,直接跳過了正解,此時可以把:可以把 Δ t \Delta t Δt 從0.99改為0.999

2.模板

#include <bits/stdc++.h>

using namespace std;

typedef pair<double, double> PDD;
const int N = 110;

int n;
PDD q[N];
double ans = 1e8;

double rand(double l, double r) {
    return (double)rand() / RAND_MAX * (r - l) + l;
}

double get_dist(PDD a, PDD b) {
    double dx = a.first - b.first;
    double dy = a.second - b.second;
    return sqrt(dx * dx + dy * dy);
}

double calc(PDD p) {  // 計算取當前點時,到所有的點的距離
    double res = 0;
    for (int i = 0; i < n; i++) res += get_dist(p, q[i]);
    ans = min(ans, res);
    return res;
}

void simulate_anneal() {  // 模擬退火
    PDD cur(rand(0, 10000), rand(0, 10000));  // 先隨機一個點 
    for (double t = 1e4; t > 1e-4; t *= 0.9) {  // 按照降溫係數進行降溫
        PDD new_point(rand(cur.first - t, cur.first + t), rand(cur.second - t, cur.second + t));  // 得到一個新的點
        double dt = calc(new_point) - calc(cur);  // 計算新的點和舊的點的差值
        if (exp(-dt / t) > rand(0, 1)) cur = new_point;  // 如果新的點更優,那麼跳到新的點去
    }
}

int main() {
    scanf("%d", &n);
    for (int i = 0; i < n; i++) scanf("%lf%lf", &q[i].first, &q[i].second);

    for (int i = 0; i < 100; i++) simulate_anneal();  // 跑100次
    printf("%.0lf\n", ans);

    return 0;
}

3.典型例題

acwing3167. 星星還是樹

題意: 在二維平面上有 n n n 個點,第 i i i 個點的座標為 ( x i , y i ) (xi,yi) (xi,yi)。請你找出一個點,使得該點到這 n 個點的距離之和最小。該點可以選擇在平面中的任意位置,甚至與這 n 個點的位置重合 1 ≤ n ≤ 100 , 0 ≤ x i , y i ≤ 10000 1≤n≤100,0≤xi,yi≤10000 1n100,0xi,yi10000

題解: 模擬退火。每次先隨機化一個點,然後按照降溫係數降低溫度,每次跳轉到一個新的點,如果新的點更優,那麼就跳到這個點去。

程式碼:

#include <bits/stdc++.h>

using namespace std;

typedef pair<double, double> PDD;
const int N = 110;

int n;
PDD q[N];
double ans = 1e8;

double rand(double l, double r) {
    return (double)rand() / RAND_MAX * (r - l) + l;
}

double get_dist(PDD a, PDD b) {
    double dx = a.first - b.first;
    double dy = a.second - b.second;
    return sqrt(dx * dx + dy * dy);
}

double calc(PDD p) {  // 計算取當前點時,到所有的點的距離
    double res = 0;
    for (int i = 0; i < n; i++) res += get_dist(p, q[i]);
    ans = min(ans, res);
    return res;
}

void simulate_anneal() {  // 模擬退火
    PDD cur(rand(0, 10000), rand(0, 10000));  // 先隨機一個點 
    for (double t = 1e4; t > 1e-4; t *= 0.9) {  // 按照降溫係數進行降溫
        PDD new_point(rand(cur.first - t, cur.first + t), rand(cur.second - t, cur.second + t));  // 得到一個新的點
        double dt = calc(new_point) - calc(cur);  // 計算新的點和舊的點的差值
        if (exp(-dt / t) > rand(0, 1)) cur = new_point;  // 如果新的點更優,那麼跳到新的點去
    }
}

int main() {
    scanf("%d", &n);
    for (int i = 0; i < n; i++) scanf("%lf%lf", &q[i].first, &q[i].second);

    for (int i = 0; i < 100; i++) simulate_anneal();  // 跑100次
    printf("%.0lf\n", ans);

    return 0;
}

acwing2424. 保齡球

題意: 給定n個擊倒保齡球的操作,每次給定a和b,表示第一個保齡球擊倒瓶子數目為a,第二個保齡球擊倒的瓶子數目為b。計分方案如下:如果第i輪a=10,那麼獎勵下一輪的a和b得分;如果第i論的a + b = 10,那麼獎勵下一輪的a的得分。現在要求重新排列這n個擊倒保齡球的操作,要求最終得分最高,求最高的得分是多少?

題解: 模擬退火。先隨機得到的一段序列的操作順序,然後每次隨機選擇a和b,表示交換a和b,如果交換完更優,那麼進行交換。

程式碼:

#include <bits/stdc++.h>

using namespace std;

typedef pair<int, int> PII;
const int N = 55;

int n, m;
PII q[N];
int ans;

int calc() {  // 計算按照當前排列的得分
    int res = 0;
    for (int i = 0; i < m; i++) {
        res += q[i].first + q[i].second;
        if (i < n) {
            if (q[i].first == 10)
                res += q[i + 1].first + q[i + 1].second;
            else if (q[i].first + q[i].second == 10)
                res += q[i + 1].first;
        }
    }
    ans = max(ans, res);
    return res;
}

void simulate_anneal() {
    for (double t = 1e4; t > 1e-4; t *= 0.99) {
        int a = rand() % m, b = rand() % m;  // 隨機找到2個交換
        int x = calc();  // 計算原來的值
        swap(q[a], q[b]);  // 交換
        if (n + (q[n - 1].first == 10) == m) {  // 比如滿足這個條件才能交換
            int y = calc();  // 計算新的值
            int delta = y - x;  // 計算差值
            if (exp(delta / t) < (double)rand() / RAND_MAX) swap(q[a], q[b]);  // 如果不滿足條件,再次交換,即跳到新的點
        } else
            swap(q[a], q[b]);
    }
}

int main() {
    cin >> n;
    for (int i = 0; i < n; i++) cin >> q[i].first >> q[i].second;
    if (q[n - 1].first == 10)
        m = n + 1, cin >> q[n].first >> q[n].second;
    else
        m = n;

    for (int i = 0; i < 100; i++) simulate_anneal();  // 進行100次模擬退火

    cout << ans << endl;
    return 0;
}

acwing2680. 均分資料

題意: 有n個正整數 a [ i ] a[i] a[i],要求將這n個數字分為m組,使得每組資料的平均數值和最平均,列印劃分完後最小的均方差。 M < = N < = 20 , 2 < = M < = 6 , 1 < = A i < = 50 M <= N <= 20, 2 <= M <= 6, 1 <= A_i <= 50 M<=N<=20,2<=M<=6,1<=Ai<=50

題解: 模擬退火+貪心。每次隨機將所有數字排序,然後隨機選擇2個數字,進行交換,計算交換後的值哪個更優來決定是否交換。calc時使用貪心的思路:維護m組數字的和,然後每次將當前數字放入最小的那組數字內,從而計算出最小值。

程式碼:

#include <bits/stdc++.h>

using namespace std;

const int N = 25, M = 10;

int n, m;
int w[N], s[M];
double ans = 1e8;

double calc() {  // 貪心計算當前每組的均方差
    memset(s, 0, sizeof s);
    for (int i = 0; i < n; i++) {  // 每次把當前值放入分組的最小值內
        int k = 0;
        for (int j = 0; j < m; j++)
            if (s[j] < s[k]) k = j;
        s[k] += w[i];
    }

    double avg = 0;
    for (int i = 0; i < m; i++) avg += (double)s[i] / m;  // 計算每組的均值
    double res = 0;
    for (int i = 0; i < m; i++) res += (s[i] - avg) * (s[i] - avg);  // 計算每組的均方差
    res = sqrt(res / m);
    ans = min(ans, res);
    return res;
}

void simulate_anneal() {
    random_shuffle(w, w + n);  // 先隨機初始序列

    for (double t = 1e6; t > 1e-6; t *= 0.95) {
        int a = rand() % n, b = rand() % n;  // 每次隨機選擇兩個位置
        double x = calc();  // 計算沒有交換時的初始值
        swap(w[a], w[b]);   // 交換
        double y = calc();  // 計算交換完後的值
        double delta = y - x;  // 計算差值
        if (exp(-delta / t) < (double)rand() / RAND_MAX) swap(w[a], w[b]);  // 如果不滿足條件,不跳,那麼就再交換一次,等價於不叫喚
    }
}

int main() {
    cin >> n >> m;
    for (int i = 0; i < n; i++) cin >> w[i];

    for (int i = 0; i < 100; i++) simulate_anneal();
    printf("%.2lf\n", ans);

    return 0;
}

ICPC 2018 Nanjing D. Country Meow

**大意:**給出空間上的n個點,求出一個點的位置,使得到這個點距離最遠的點的距離最小

思路: 模擬退火。本題和acwing3167. 星星還是樹基本一樣。

#include <bits/stdc++.h>

using namespace std;

typedef pair<double, double> PDD;
const int N = 110;

struct Point {
    double x, y, z;
}q[N];
int n;
double ans = 1e8;

double rand(double l, double r) {
    return (double)rand() / RAND_MAX * (r - l) + l;
}

double get_dist(Point a, Point b) {
    double dx = a.x - b.x;
    double dy = a.y - b.y;
    double dz = a.z - b.z;
    return sqrt(dx * dx + dy * dy + dz * dz);
}

double calc(Point p) {  // 計算取當前點時,到所有的點的距離
    double res = 0;
    double tmp_max = 0;
    for (int i = 0; i < n; i++) tmp_max = max(tmp_max, get_dist(p, q[i]));
    ans = min(ans, tmp_max);
    return tmp_max;
}

void simulate_anneal() {  // 模擬退火
    Point cur = {rand(-1e5, 1e5), rand(-1e5, 1e5), rand(-1e5, 1e5)};  // 先隨機一個點 
    for (double t = 2e5; t > 1e-4; t *= 0.99) {  // 按照降溫係數進行降溫
        Point new_point= {rand(cur.x - t, cur.x + t), rand(cur.y - t, cur.y + t), rand(cur.z - t, cur.z + t)};  // 得到一個新的點
        double dt = calc(new_point) - calc(cur);  // 計算新的點和舊的點的差值
        if (exp(-dt / t) > rand(0, 1)) cur = new_point;  // 如果新的點更優,那麼跳到新的點去
    }
}

int main() {
    scanf("%d", &n);
    for (int i = 0; i < n; i++) scanf("%lf%lf%lf", &q[i].x, &q[i].y, &q[i].z);

    while ((double)clock() / CLOCKS_PER_SEC < 0.8) simulate_anneal();  // 跑100次
    printf("%.10lf\n", ans);

    return 0;
}