1. 程式人生 > >2018華為軟挑--模擬退火+FF解決裝箱問題【C++程式碼】

2018華為軟挑--模擬退火+FF解決裝箱問題【C++程式碼】

演算法簡介:

        裝箱問題是一個NP完全問題,求解全域性最優解有很多種方法:遺傳演算法、禁忌搜尋演算法、蟻群演算法、模擬退火演算法等等,本次使用模擬退火,它的優點是在引數合適的情況下基本上可以100%得到全域性最優解,缺點是相較於其他演算法,其穩定速度較慢。

        如果你對退火的物理意義還是暈暈的,沒關係我們還有更為簡單的理解方式。想象一下如果我們現在有下面這樣一個函式,現在想求函式的(全域性)最優解。如果採用貪心策略,那麼從A點開始試探,如果函式值繼續減少,那麼試探過程就會繼續。而當到達點B時,顯然我們的探求過程就結束了(因為無論朝哪個方向努力,結果只會越來越大)。最終我們只能找打一個區域性最後解B。

        可以看出模擬退火其實也是一種貪心演算法,但是它的搜尋過程引入了隨機因素。模擬退火演算法以一定的概率來接受一個比當前解要差的解,因此有可能會跳出這個區域性的最優解,達到全域性的最優解。以上圖為例,模擬退火演算法在搜尋到區域性最優解B後,會以一定的概率接受向右繼續移動。也許經過幾次這樣的不是區域性最優的移動後會到達B 和C之間的峰點,於是就跳出了局部最小值B。

演算法過程:

        根據Metropolis準則,粒子在溫度T時趨於平衡的概率為exp(-ΔE/(kT)),其中E為溫度T時的內能,ΔE為其改變數,k為Boltzmann常數。Metropolis準則常表示為

        Metropolis準則表明,在溫度為T時,出現能量差為dE的降溫的概率為P(dE),表示為:P(dE) = exp( dE/(kT) )。其中k是一個常數,exp表示自然指數,且dE<0。所以P和T正相關。這條公式就表示:溫度越高,出現一次能量差為dE的降溫的概率就越大;溫度越低,則出現降溫的概率就越小。又由於dE總是小於0(因為退火的過程是溫度逐漸下降的過程),因此dE/kT < 0 ,所以P(dE)的函式取值範圍是(0,1) 。隨著溫度T的降低,P(dE)會逐漸降低。

        我們將一次向較差解的移動看做一次溫度跳變過程,我們以概率P(dE)來接受這樣的移動。也就是說,在用固體退火模擬組合優化問題,將內能E模擬為目標函式值 f,溫度T演化成控制引數 t,即得到解組合優化問題的模擬退火演演算法:由初始解 i 和控制引數初值 t 開始,對當前解重複“產生新解→計算目標函式差→接受或丟棄”的迭代,並逐步衰減 t 值,演算法終止時的當前解即為所得近似最優解。

總結起來就是:
若f( Y(i+1) ) <= f( Y(i) )  (即移動後得到更優解),則總是接受該移動;
若f( Y(i+1) ) > f( Y(i) )  (即移動後的解比當前解要差),則以一定的概率接受移動,而且這個概率隨著時間推移逐漸降低(逐漸降低才能趨向穩定)相當於上圖中,從B移向BC之間的小波峰時,每次右移(即接受一個更糟糕值)的概率在逐漸降低。如果這個坡特別長,那麼很有可能最終我們並不會翻過這個坡。如果它不太長,這很有可能會翻過它,這取決於衰減 t 值的設定。

 舉一個例子:

        求函式f(x)=11*sin(6*x)+7*cos(5*x) , x∈[0,2*pi] 的最小值。


由函式影象可以看出存在很多極小值,要求全域性最優可以採用模擬退火,具體程式碼如下:

//f(x)=11*sin(6*x)+7*cos(5*x),x∈[0,2*pi],求最小值,真實最小值為-17.833  
#include <iostream>  
#include <math.h>  
#include <time.h>  

#define pi 3.14159  
#define num 30000 //迭代次數  
double k = 0.01;
double r = 0.99; //用於控制降溫的快慢  
double T = 200; //系統的溫度,系統初始應該要處於一個高溫的狀態  
double T_min = 2;//溫度的下限,若溫度T達到T_min,則停止搜尋  
				 //返回指定範圍內的隨機浮點數  

double rnd(double dbLow, double dbUpper)//產生(dbLow,dbUpper)之間的隨機數
{
	double dbTemp = rand() / ((double)RAND_MAX + 1.0);
	return dbLow + dbTemp*(dbUpper - dbLow);
}

double func(double x)//目標函式  
{
	return 11 * sin(6 * x) + 7 * cos(5 * x);
}

int main()
{
	double best = func(rnd(0.0, 2 * pi));
	double dE, current;
	int i;
	srand((unsigned)(time(NULL)));//用當前時間點初始化隨機種子,防止每次執行的結果都相同

	while (T > T_min)
	{
		for (i = 0; i < num; i++)
		{
			current = func(rnd(0.0, 2 * pi));//產生新解
			dE = current - best;

			if (dE < 0) //表達移動後得到更優解,則總是接受移動  
				best = current;
			else
			{
				// 函式exp( dE/T )的取值範圍是(0,1) ,dE/T越大,則exp( dE/T )也越大  
				if (exp(-dE / (T*k)) > rnd(0.0, 1.0))//有一定概率接受較差解
					best = current;
			}
		}
		T = r * T;//降溫退火 ,0<r<1 。r越大,降溫越慢;r越小,降溫越快  
	}

	printf("最小值是 %f\n", best);
	return 0;
}

執行結果:


在程式執行5秒左右,結果可以看出模擬退火可以求出全域性最優解。

模擬退火解決裝箱問題:

        幾個需要修改的核心問題:

        1、目標函式:因為需要箱子數目最小,所以設定為物理伺服器的個數PsyPsNum。

        2、構造初始解:利用貪心演算法FF(對應於函式distribution(temp))先進行一次裝箱,得到物理伺服器個數的解。

void distribution(FlavorS flavors[])
{
	PsyPsNum = FlavorSBox(flavors, totalPreNum, ECS.cpu, ECS.mem);
}

int FlavorSBox(FlavorS goods[], int n, int cpu_num, int mem) //裝箱問題貪心演算法
{
	int num = 0;
	GNode *pg, *t;
	GBox *hbox = NULL, *pb, *qb;
	int i;
	for (i = 0; i < n; i++) /////////////////遍歷虛擬機器資訊陣列
	{
		pg = (GNode *)malloc(sizeof(GNode)); ///////////////分配貨物節點單元
		//pg->s = goods[i].s;
		pg->link = NULL; //貨物節點初始化
		if (!hbox)		 //若一個物理伺服器都沒有
		{
			hbox = (GBox *)malloc(sizeof(GBox));
			hbox->remainder = cpu_num; //物理伺服器可以容納的CPU
			hbox->mem = mem;		   //物理伺服器可以容納的記憶體
			hbox->head = NULL;
			hbox->next = NULL;
			num++; //物理伺服器數量加1
			hbox->box_no = num;
		}
		qb = pb = hbox; //都指向物理伺服器頭
		while (pb)		//找物理伺服器
		{
			if (pb->remainder >= goods[i].cpu && pb->mem >= goods[i].mem) /////////////////////////////能裝下
				break;													  //找到箱子,跳出while
			else
			{

				qb = pb;
				pb = pb->next; //qb是前驅
			}

		} /////////////////////////////////////遍歷物理伺服器結束

		if (pb == NULL) /////////////////////需要新物理伺服器
		{
			pb = (GBox *)malloc(sizeof(GBox)); //分配物理伺服器
			pb->head = NULL;
			pb->next = NULL;
			pb->remainder = cpu_num;
			pb->mem = mem;
			qb->next = pb; //前驅指上
			num++;		   //物理伺服器數量加1
			pb->box_no = num;
		}

		if (!pb->head) //如果物理伺服器裡沒貨
		{
			pb->head = pg;
			t = pb->head;
			goods[i].PsId = pb->box_no; //將虛擬機器與物理伺服器編號關聯起來
										//cout << goods[i].s << "裝入" << goods[i].PsId << "物理伺服器" << endl;
		}
		else
		{
			t = pb->head;
			while (t->link)
				t = t->link; //尾插
			t->link = pg;
			goods[i].PsId = pb->box_no; //將虛擬機器與物理伺服器編號關聯起來
										//cout << goods[i].s << "裝入" << goods[i].PsId << "物理伺服器" << endl;
		}
		pb->remainder -= goods[i].cpu;
		pb->mem -= goods[i].mem;
	}
	return num;
}

        3、產生新解:利用交換箱子間的排列順序(對應於函式generateNew(50, temp)),貪心放入,得到新的結果。

//swapTimes表示交換次數,flavors表示需要交換的物件
void generateNew(int swapTimes, FlavorS flavors[])
{
	for (int i = 0; i < swapTimes; i++)
	{
		int posx = rand() % totalPreNum;
		int posy = rand() % totalPreNum;
		swap(flavors[posx], flavors[posy]);
	}
}

        4、注意每一次產生新解,不一定都要接受結果,所以選擇複製一個flavors陣列為temp陣列,在temp中產生新解,只有選擇接受新解才將temp的結果複製進flavors中,否則flavors陣列不變。

模擬退火程式碼:

void SimulatedFire(FlavorS flavors[])
{
	int best = PsyPsNum;
	cout << "before fire:" << PsyPsNum << endl;

	const int LL = 300;
	double k = 0.1;
	double r = 0.97;	 //用於控制降溫的快慢
	double T = 300;		 //系統的溫度,系統初始應該要處於一個高溫的狀態
	double T_min = 0.1; //溫度的下限,若溫度T達到T_min,則停止搜尋
						 //返回指定範圍內的隨機浮點數
	int dE, current;
	srand((unsigned)(time(NULL)));

	FlavorS *temp = new FlavorS[totalPreNum];
	for (int i = 0; i < totalPreNum; i++)
	{
		temp[i].s = flavors[i].s;
		temp[i].cpu = flavors[i].cpu;
		temp[i].mem = flavors[i].mem;
		temp[i].PsId = flavors[i].PsId;
	}

	while (T > T_min)
	{
		for (int i = 0; i < LL; i++)
		{
			generateNew(50, temp);
			distribution(temp);
			current = PsyPsNum;
			dE = current - best;

			if (dE <= 0) //表達移動後得到更優解,則總是接受移動
			{
				best = current;
				for (int i = 0; i < totalPreNum; i++)
				{
					flavors[i].s = temp[i].s;
					flavors[i].cpu = temp[i].cpu;
					flavors[i].mem = temp[i].mem;
					flavors[i].PsId = temp[i].PsId;
				}
			}	
			else
			{
				// 函式exp( dE/T )的取值範圍是(0,1) ,dE/T越大,則exp( dE/T )也越大
				if (exp(-dE / (T * k)) > rnd(0.0, 1.0))
				{
					best = current;
					for (int i = 0; i < totalPreNum; i++)
					{
						flavors[i].s = temp[i].s;
						flavors[i].cpu = temp[i].cpu;
						flavors[i].mem = temp[i].mem;
						flavors[i].PsId = temp[i].PsId;
					}
				}
			}
		}
		T = r * T; //降溫退火 ,0<r<1 。r越大,降溫越慢;r越小,降溫越快
	}
	PsyPsNum = best;
	delete[] temp;
	cout << "after fire:" << PsyPsNum << endl;
}

當設定輸入引數為:


模擬退火前後需要物理伺服器個數結果為:


可以看到比貪心演算法少使用了2個物理伺服器就將所有的虛擬伺服器裝下了,雖然執行時間略久,但實現了裝箱問題的最優解。