sscms/siteserver 站點首頁-公司簡介呼叫
技術標籤:ACM--雜項
模擬退火
1.演算法描述
1.1 概述
簡單說,模擬退火是一種隨機化演算法,用於求函式的極值。當一個問題的方案數量極大(甚至是無窮的)而且不是一個單峰函式時,我們常使用模擬退火求解。
它與爬山演算法最大的不同是,在尋找到一個區域性最優解時,賦予了它一個跳出去的概率,也就有更大的機會能找到全域性最優解。
在 OI 領域,對應的,每次隨機出一個新解,如果這個解更優,則接受它,否則以一個與溫度和與最優解的差相關的概率接受它。
以下圖片來自:https://oi-wiki.org/misc/simulated-annealing/
1.2 相關引數
初始溫度:
T
0
T_0
結束溫度: 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越大越容易接受。針對具體問題為:
- 如果是求凹函式的最值,那麼判斷條件寫為:
if (exp(-dt / t) > rand(0, 1)) cur = new; // 如果概率大於(0 ~ 1),那麼就跳到新的點
else continue; // 否則不跳
- 如果是求凸函式的最值,那麼判斷條件寫為:
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 1≤n≤100,0≤xi,yi≤10000
題解: 模擬退火。每次先隨機化一個點,然後按照降溫係數降低溫度,每次跳轉到一個新的點,如果新的點更優,那麼就跳到這個點去。
程式碼:
#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;
}