題目描述
現在有n枚金幣,它們可能會有不同的價值,現在要把它們分成兩部分,要求這兩部分金幣數目之差不超過1,問這樣分成的兩部分金幣的價值之差最小是多少?
題目分析
分析發現,我們可以每次交換在兩個部分中的兩個元素,然後求兩部分差最小的情況.
但是觀察的範圍不大於,那麼在數據最強的情況下,搜索的複雜度可以達到的水平,顯然是承受不了的.
又因爲這是一個最優解問題,所以我們考慮模擬退火算法.
模擬退火算法
模擬退火算法來源於固體退火原理,將固體加溫至充分高,再讓其徐徐冷卻,加溫時,固體內部粒子隨溫升變爲無序狀,內能增大,而徐徐冷卻時粒子漸趨有 序,在每個溫度都達到平衡態,最後在常溫時達到基態,內能減爲最小。根據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.分別將兩個狀態的答案計算出來,並作差得到;
4.根據題目要求,貪心的決定是否更換答案(有些題目是改變狀態). 即:選擇最優解;如果無法替換答案,則根據一定概率替換答案. 即運用到上述的平衡概率(表示的次方)隨機的決定是否替換.
每一次操作後,進行降溫操作。即:將溫度乘上某一個係數,一般是隨具體題目隨緣定.
我們會發現,新狀態離最優解越近,溫度越高,越容易被接納
僞代碼實現如下:
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的符號問題:
自然對數的底的冪必須爲負數,所以有時候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;
}
感謝算法支持: