模擬退火算法解析

前言

模擬退火 \(Simulated\) \(Annealing\) , 簡稱 \(SA\) ,最先在 \(1953\) 年由 \(N. Metropolis\) 提出,後經優化獲得如今普遍應用的算法,應用在不少領域當中。算法

本文題目連接函數

算法思想

模擬退火是隨機化搜索的一種,若隨機化搜索寫得好,則能夠實現高效率和答案的正確率高(雖然說不是 \(100\%\) )。不少時候在想不出解決辦法,或方法的時間複雜度出現極大狀況時,可以使用模擬退火。所說是有較大概率正確,但仍是有疏漏,那麼能夠屢次試驗來更加接近準確地求出這個值(仍是要看運氣)。優化

模擬退火,顧名思義,是模擬工業上固體降溫的過程。先將固體加溫到必定的溫度後,在按照適當的溫度進行冷卻,冷卻到改物體想要達到的狀態。溫度下降地越慢,則該物體的質量約高,由於分子在因熱加速運動中找到了更加合適的位置。當溫度逐漸下降,分子運動減緩,達成目的。ui

那麼這一現象被科學家與計算機算法所聯繫起來,就成了如今的模擬退火。spa

網上的一張圖,模擬退火可視化:
在這裏插入圖片描述code

模擬退火有幾個很關鍵的參數,這幾個參數決定了模擬退火的優劣。blog

  1. 隨機種子 \(seed\) ,可使用 \(19260817\) ,或是時間,不推薦使用其餘參數,極可能會下降正確率。
  2. 初始溫度 \(TempHigh\) ,通常取 \(100\)\(10000\) 不等,但做者更加傾向於 \(2000\)\(3000\) 的數字。
  3. 目標溫度 \(TempLow\) ,通常取 \(1^{-10}\)\(1^{-15}\)
  4. 溫度變化率 \(TempLess\) ,通常取 \(0.99\)\(0.9999\) 。建議不取太大,效率不高。
#define Seed 19260817
#define TempLess 0.9975
#define TempHigh 2879.0
#define TempLow 1e-12

來看看降火的主體部分。圖片

void SA() {
	double temp = TempHigh;//初始化溫度
	定義初始狀態;
	while(temp > TempLow) {//打到降溫條件
		double nowans = Get_Ans(當前狀態);//更新最優解
		double diff = nowans - ans;//與當前答案的差值
		if(diff > 0) {//比當前答案更優
			轉移狀態;
			ans = nowans;//更新答案
		}
		else if(exp(-diff / temp) * RAND_MAX < rand()) {
//接受這個解,爲何這樣寫請見例題部分
			轉移狀態;
		}
		temp *= TempLess;//降溫
	}
}

模擬退火查詢的是多峯函數的最值。ci

如下曲線是解析式爲 \(y=0.05x^3-0.5x^2\) 的函數的圖像:
在這裏插入圖片描述
先來考慮貪心的作法:
在這裏插入圖片描述
當到達點 \(A\) 時,程序會選擇更高的一個點,那麼會從 \(A\) 點到達 \(B\) 點,而再從 \(B\) 點俯瞰,看到了點 \(C\) ,因爲 \(C\) 的縱座標比 \(B\) 小,因此點 \(B\) 不會到達點 \(C\) 。換句話說,該程序 \(100\%\) 不會接受點 \(C\) ,進而更不會到達點 \(D\) 。不難發現,這時候找到的局部最優解並非全局最優解。get

而模擬退火再次作出了改進。假設初始位置在點 \(A\) ,則會基於 \(A\) 作出左右擺動,通過數次擺動後到達 \(B\) 。再進一步擺動,假設擺動到了 \(C\) 點,可是 \(C\) 的縱座標比 \(B\) 小,會以必定概率以 \(C\) 的縱座標來接受 \(C\) 。進而在以一樣的方式擺動到點 \(D\) ,找到更高點。

因爲是該算法隨機性較高,因此多跑幾遍該函數。

下面結合一道例題更加深刻地探究,題目連接已在上文給出。

題意

\(n\) 個圓, \(m\) 個點,請求出一個半徑不超過 \(r\) 的圓,使得與這 \(n\) 個圓沒有交集,且可以覆蓋的點最大。

思路

此題的答案圓的圓心並不知足是整數,且由橫縱座標兩個值來影響,並不具備規律。這樣的問題一般使用模擬退火來解決。

void SA() {
	double temp = TempHigh;//初始化溫度
	定義初始狀態;
	while(temp > TempLow) {//打到降溫條件
		double nowans = Get_Ans(當前狀態);//更新最優解
		double diff = nowans - ans;//與當前答案的差值
		if(diff > 0) {//比當前答案更優
			轉移狀態;
			ans = nowans;//更新答案
		}
		else if(exp(-diff / temp) * RAND_MAX < rand()) {//接受這個解
			轉移狀態;
		}
		temp *= TempLess;//降溫
	}
}

如上,初始狀態包含了橫座標和縱座標,爲了提升正確率與效率,設爲全部點的橫縱座標的平均值。

\(GetAns\) 函數也很簡單,先肯定半徑,半徑爲這個點到各個圓的切線的距離的最小值,即兩個圓心的距離減去當前枚舉到的這個圓的半徑。後枚舉每一個點,若這個點被覆蓋則 \(res++\) ,最後返回 \(res\)

double Get_Ans(double x, double y) {
	double res = 0;
	double rkill = r;
	for(int i = 1; i <= n; i++)//枚舉圓
		rkill = Min(rkill, Dist_Cartesian(XC(i), YC(i), x, y) - RC(i));
	for(int i = 1; i <= m; i++)//枚舉點
		if(Dist_Cartesian(XE(i), YE(i), x, y) <= rkill)
			res += 1.0;
	return res;
}

有了 \(GetAns\) 函數,主題部分也很快能出來。

void SA() {
	double temp = TempHigh, ansx = initx, ansy = inity;//降溫前初始化
	while(temp > TempLow) {
		double nowx = ansx + ((rand() << 1) - RAND_MAX) * temp;
		double nowy = ansy + ((rand() << 1) - RAND_MAX) * temp;
		double nowans = Get_Ans(nowx, nowy);
		double diff = nowans - ans;
		if(diff > 0) {
			initx = nowx;
			inity = nowy;
			ansx = nowx;
			ansy = nowy;
			ans = nowans;
		}
		else if(exp(-diff / temp) * RAND_MAX < rand()) {
			ansx = nowx;
			ansy = nowy;
		}
		temp *= TempLess;
	}
}

首先來看這段代碼

double nowx = ansx + ((rand() << 1) - RAND_MAX) * temp;
double nowy = ansy + ((rand() << 1) - RAND_MAX) * temp;

由答案左右擺動,生成新的當前狀態 \(nowx\)\(nowy\) ,擺動幅度是隨機的,應該是由分子作無規則運動而來。乘上 \(temp\) 當前溫度是由分子在越熱的環境中,運動得越快而得來。

緊接着兩行就是求出當前狀態的答案,在求出它與當前最優解的差值。

第一個 \(if\) 是當前這個局部解大於當前最優解,則用當前最優的局部解來更新最優解。

重點是下一個 \(if\) ,這行代碼就是它與貪心的不一樣,以必定概率接受這個解,在用它更新當前狀態,進行左右擺動,從而找到局部更優解,更加接近總體最優解。其條件的優越性由 \(Metropolis\) 接受準則給出。也就是 \(else\) \(if\) 中的條件:

exp(-diff / temp) * RAND_MAX < rand()

思路整理完了,此題並無多少思惟難度,可是須要對上述幾個參數進行調整,能夠多總結一些正確率大的參數,以備下次使用

C++代碼

#include <cmath>
#include <cstdio>
#include <cstdlib>
#define Min(a, b) ((a) < (b) ? (a) : (b)) 
#define Seed 19260817//隨機種子
#define TempLess 0.9975//溫度變化率
#define TempHigh 2879.0//初始溫度
#define TempLow 1e-12//目標溫度
void Quick_Read(double &N) {//double快速讀入
	N = 0.0;
	double now, wei = 0.1;
	bool op = false;
	char c = getchar();
	while(c < '0' || c > '9') {
		if(c == '-')
			op = -1;
		c = getchar();
	}
	while(c >= '0' && c <= '9') {
		N = N * 10.0 + (c ^ 48) * 1.0;
		c = getchar();
	}
	if(c == '.') {
		c = getchar();
		while(c >= '0' && c <= '9') {
			N += (c ^ 48) * wei;
			wei /= 10.0;
			c = getchar();
		}
	}
	if(op)
		N = -N;
}
const int MAXN = 15;
const int MAXM = 1e3 + 5;
struct Circle {//題目中的圓
	double Abscissa_C, Ordinate_C, Radius_C;
	#define XC(x) buildings[x].Abscissa_C
	#define YC(x) buildings[x].Ordinate_C
	#define RC(x) buildings[x].Radius_C
};
Circle buildings[MAXN];
struct Enemy {//題目中的點
	double Abscissa_E, Ordinate_E;
	#define XE(x) foe[x].Abscissa_E
	#define YE(x) foe[x].Ordinate_E
};
Enemy foe[MAXM];
double n, m, r;
double initx, inity;//記錄答案的橫縱座標
double ans;//答案
double Dist_Cartesian(double XA, double YA, double XB, double YB) {//兩點間距離公式
	double frontx = (XA - XB) * (XA - XB);
	double fronty = (YA - YB) * (YA - YB);
	double dist = sqrt(frontx + fronty);
	return dist;
}
double Get_Ans(double x, double y) {//找到當前狀態的答案
	double res = 0;
	double rkill = r;
	for(int i = 1; i <= n; i++)//求出最大半徑
		rkill = Min(rkill, Dist_Cartesian(XC(i), YC(i), x, y) - RC(i));
	for(int i = 1; i <= m; i++)//求出被圓覆蓋的點
		if(Dist_Cartesian(XE(i), YE(i), x, y) <= rkill)
			res += 1.0;
	return res;
}
void SA() {
	double temp = TempHigh, ansx = initx, ansy = inity;//初始化
	while(temp > TempLow) {//降溫
		double nowx = ansx + ((rand() << 1) - RAND_MAX) * temp;//當前狀態x
		double nowy = ansy + ((rand() << 1) - RAND_MAX) * temp;//當前狀態y
		double nowans = Get_Ans(nowx, nowy);//當前局部答案
		double diff = nowans - ans;//當前答案與最優解的差值
		if(diff > 0) {//比當前最優解更優則更新最優解
			initx = nowx;
			inity = nowy;
			ansx = nowx;
			ansy = nowy;
			ans = nowans;
		}
		else if(exp(-diff / temp) * RAND_MAX < rand()) {
//按照Metropolis接受準則接受改狀態
			ansx = nowx;
			ansy = nowy;
		}
		temp *= TempLess;//降溫
	}
}
void Cool_Down() {
	int frequ = 6;
	while(frequ--)//隨機化算法儘可能多跑幾回
		SA();
}
void Make_Seed() {//生成隨機種子
    srand(Seed);
}
void Read() {//輸入
    Quick_Read(n);
    Quick_Read(m);
    Quick_Read(r);
    for(int i = 1; i <= n; i++) {
    	Quick_Read(XC(i));
    	Quick_Read(YC(i));
    	Quick_Read(RC(i));
	}
	for(int i = 1; i <= m; i++) {
		Quick_Read(XE(i));
		Quick_Read(YE(i));
		initx += XE(i);
		inity += YE(i);
	}
	initx /= m;//以平均值開始提升效率與準確率
	inity /= m;
}
void Write() {//輸出
	printf("%.0lf", ans);
}
int main() {
	Make_Seed();
	Read();
	Cool_Down();
	Write();
	return 0;
}
相關文章
相關標籤/搜索