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个物理服务器就将所有的虚拟服务器装下了,虽然运行时间略久,但实现了装箱问题的最优解。

  • 9
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值