模擬退火學習報告(P3878 [TJOI2010]分金幣)

P3878 [TJOI2010]分金幣

題目描述

現在有n枚金幣,它們可能會有不同的價值,現在要把它們分成兩部分,要求這兩部分金幣數目之差不超過1,問這樣分成的兩部分金幣的價值之差最小是多少?

題目分析

分析發現,我們可以每次交換在兩個部分中的兩個元素,然後求兩部分差最小的情況.

但是觀察nn的範圍不大於3030,那麼在數據最強的情況下,搜索的複雜度可以達到O(2n)O(2^n)的水平,顯然是承受不了的.

又因爲這是一個最優解問題,所以我們考慮模擬退火算法.

模擬退火算法

模擬退火算法來源於固體退火原理,將固體加溫至充分高,再讓其徐徐冷卻,加溫時,固體內部粒子隨溫升變爲無序狀,內能增大,而徐徐冷卻時粒子漸趨有 序,在每個溫度都達到平衡態,最後在常溫時達到基態,內能減爲最小。根據Metropolis準則,粒子在溫度T時趨於平衡的概率爲 e(-ΔE/(kT)),其中E爲溫度T時的內能,ΔE爲其改變量,k爲Boltzmann常數。用固體退火模擬組合優化問題,將內能E模擬爲目標函數值 f,溫度T演化成控制參數t,即得到解組合優化問題的模擬退火算法:由初始解i和控制參數初值t開始,對當前解重複“產生新解→計算目標函數差→接受或捨棄”的迭代,並逐步衰減t值,算法終止時的當前解即爲所得近似最優解,這是基於蒙特卡羅迭代求解法的一種啓發式隨機搜索過程。退火過程由冷卻進度表 (Cooling Schedule)控制,包括控制參數的初值t及其衰減因子Δt、每個t值時的迭代次數L和停止條件S。

我們還可以用一個形象的比喻來描述模擬退火算法:兔子喝醉了。它隨機地跳了很長時間. 這期間,它可能走向高處,也可能踏入平地. 但是,它漸漸清醒了並朝最高方向跳去. 這就是模擬退火.

由上述可以發現,模擬退火大致有這些步驟:

1.設置初始溫度T,初始符合條件的答案;
2.通過某種神奇的方式,找到另一個符合條件的新狀態;
3.分別將兩個狀態的答案計算出來,並作差得到ΔEΔE
4.根據題目要求,貪心的決定是否更換答案(有些題目是改變狀態). 即:選擇最優解;如果無法替換答案,則根據一定概率替換答案. 即運用到上述的平衡概率exp(ΔE/T)exp(ΔE/T)(表示eeΔE/TΔE/T次方)隨機的決定是否替換.
每一次操作後,進行降溫操作。即:將溫度TT乘上某一個係數,一般是0.9850.9990.985−0.999隨具體題目隨緣定.

我們會發現,新狀態離最優解越近,溫度越高,越容易被接納

僞代碼實現如下:

esp=1e-15;//終止溫度
T=初始溫度;
while(T>esp)
{
    now=從當前最優狀態隨機更新的一個狀態;
    delta=calc(now)-calc(ans);
    if(delta與題目要求的滿足更優)ans=now;
    else if(exp(delta/T)*RAND_MAX>rand())ans=now;//PS:這裏的delta前面可能要加'-';也可能不是對最優解的改變而是對狀態的改變(限於最優解記錄狀態的時候)
    T*=t0;//t0一般在0.985-0.999之間,根據具體題目時間,隨緣調試。。。
}

關於上述delta的符號問題:

自然對數的底ee的冪必須爲負數,所以有時候delta要加負號,有時候不用.


以上,我們可以假定狀態是當前的差值,每次更新狀態時就交換一組元素即可.

用ans記錄最優解,所以ans的值不改變,改變的是狀態.

程序實現

#include<bits/stdc++.h>
#define esp 1e-8//escape溫度也是隨緣
#define dt 0.998
#define ll long long
using namespace std;
ll tot1,tot2,a[40],ans;
int n,len1,len2;
void SA(){
	double T=1200;
	while(T>esp){
		int x=(int )(rand()%len1+1),y=(int )(rand()%len2+len1+1);
		ll delta=abs((tot1+a[y]-a[x])-(tot2+a[x]-a[y]))-ans;//隨機新狀態
		if(delta<0){
			tot1=tot1+a[y]-a[x];
			tot2=tot2+a[x]-a[y];
			ans=abs(tot1-tot2);
			swap(a[x],a[y]);
		}
		else if(exp((-delta)/T)*RAND_MAX>rand()){//RAND_MAX表示rand函數能夠取得到的最大值,這是對是否更新狀態的估值;exp表示自然對數的底的k次方
			tot1=tot1+a[y]-a[x];
			tot2=tot2+a[x]-a[y];
//			ans=abs(tot1-tot2);此行有誤
			swap(a[x],a[y]);//改變的是狀態,不是用來記錄最優解的ans 
		}
		T*=dt;
	}
}
int main(){
//	freopen("make.in","r",stdin);
//	freopen("1.out","w",stdout);
	srand((int )time(0));//隨機時間種子
	int T;
	scanf("%d",&T);
	for(int i=1;i<=T;i++){
		len1=0,len2=0;
		memset(a,0,sizeof a);
		tot1=0,tot2=0,ans=0;//沒初始化就完了
		scanf("%d",&n);
		len1=n/2;
		len2=n-len1;
		for(int i=1;i<=n;i++){
			scanf("%lld",&a[i]);
			if(i<=len1)tot1+=a[i];
			else if(i>len1)tot2+=a[i];
		}
		ans=abs(tot1-tot2);
		if(n==1){printf("%lld\n",a[1]);continue;}
		for(int i=1;i<=20;i++)SA();//SA是模擬退火英文Simulate Anneal的縮寫
		printf("%lld\n",ans);
	}
	return 0;
} 

感謝算法支持:

SA算法介紹本家

rand()和srand()的用法

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章