课程名称 | 算法设计与分析 | 任课老师 | 张子臻 |
---|---|---|---|
年级 | 2016级 | 专业(方向) | 计算机应用 |
学号 | 16340030 | 姓名 | 陈斯敏 |
电话 | 15917173057 | [email protected] | |
开始日期 | 2018.12.18 | 完成日期 | 2018.12.22 |
Capacitated Facility Location Problem
Suppose there are n facilities and m customers. We wish
to choose:
(1) which of the n facilities to open ?
(2) the assignment of customers to facilities ?
The objective is to minimize the sum of the opening cost and the assignment cost.
note: The total demand assigned to a facility must not exceed its capacity.
题目要求给定相应的工厂和顾客,每个工厂有相应的容量(可容纳顾客需求)和相应的开启费用,而每个顾客有相应的需求(固定的)和任务费用(因工厂而改变),现在要对每个工厂进行分配顾客,要求所有顾客都要分配到工厂,然后工厂的容量不能超过它所分配到的顾客的需求之和,同时希望工厂开启费用和顾客任务费用总和最小,每一份数据的输入格式如下:
因为题目要求希望求出最优的算法,所以我们很容易可以想到使用贪心算法进行求解,但是贪心策略的选取很关键。对于该问题,贪心算法并不一定能求出最优解法,但是可以在较短的时间求得一个可满足解且该解的优劣程度不会太差。
接下来,讲一下实现该算法的流程和贪心策略:
一、贪心算法流程:
二、贪心策略
采用的贪心选取策略是选取性价比最高的操作,因为需要满足的需求是固定的,而产生的费用是不固定的,所以我们希望性价比最高当然是费用/需求最少,那么,我们每次分配顾客时,便会产生相应的费用,也会满足相应的需求,便可以遍历所有可进行的操作,选取其中费用需求比最小(性价比最高的)的操作,具体公式如下:
GreadyVal = assignment / demand
此处,我们除了考虑对应的客户任务费用,工厂的开启费用也需要考虑进去,这里需要进行判断,如果该工厂并未开启,则需要加上OpenCost,如果已经开启(分配过用户),那么我们便不需要加上这部分的费用,所以我们进行改进,总的运算公式如下:
GreadyVal = assignment / demand
if not isOpen:
GreadyVal += OpenCost / demand
三、贪心算法的优缺点
优点:
缺点:
四、算法实现的代码
代码地址:https://github.com/chensm9/Algorithm-project/blob/master/Greedy.cpp
这里贴出主要的贪心算法实现函数:
// 贪心算法流程
int Greedyrun() {
// 需要遍历“顾客数量”次,以此达到分配完所有的顾客
for (int i = 0; i < Cnum; i++) {
int greedyVal = INT_MAX;
int c_id = -1, f_id = -1;
// 遍历所有的用户和工厂
for (int j = 0; j < Cnum; j++) {
// 该用户已经被分配
if (CList[j].belongTo != -1)
continue;
// 遍历所有的可容纳该用户的工厂
for (int k = 0; k < Fnum; k++) {
if (CList[j].demand <= FList[k].leftCapacity) {
int tempVal = assignmentMap[k][j] / CList[j].demand;
if (FList[k].leftCapacity == FList[k].Capacity)
tempVal += FList[k].OpenCost / CList[j].demand;
// 判断性价比是否优于当前最优操作
if (greedyVal > tempVal) {
greedyVal = tempVal;
c_id = j;
f_id = k;
}
}
}
}
// 这一轮最后被选取的操作
FList[f_id].leftCapacity -= CList[c_id].demand;
CList[c_id].belongTo = f_id;
}
int bestSolution = evaluate(FList, CList);
return bestSolution;
}
局部搜索法的过程是对一个初始化的解反复进行相关领域操作,直到得到比该解更优的解,然后一直迭代下去,直到逼近最优的解,同样的,该算法也不一定能够得到最优的解,且其解与我们所选取的初始解和进行的邻域操作相关,但因为进行多次迭代能够得到比初始解更优的解,那么我们很容易想到可以将上面贪心算法得到的解作为初始解来进行迭代,那么最后得到的结果一定比贪心算法更优。
一、局部搜索算法流程:
二、相关邻域操作
这里为了效果较好,我采用了两种邻域操作:
还有N的取值,即未找到更优解的次数N,需要取多少次,此处我取的是1000。
三、局部搜索法的优缺点
优点:
缺点:
四、算法实现的代码
代码地址:https://github.com/chensm9/Algorithm-project/blob/master/HC.cpp
这里主要贴出局部搜索函数部分的代码:
// 局部搜索法
int HCrun() {
// initBestSolution();
// 利用贪心算法产生一个解
Greedyrun();
srand((int)time(0));
int flag = 0;
int bestSolution = evaluate(FList, CList);
// cout << "当前最好:" << bestSolution << endl;
Factility* newFList = new Factility[Fnum];
Customer* newCList = new Customer[Cnum];
while(flag < 10000) {
for (int i = 0; i < Fnum; i++)
newFList[i] = FList[i];
for (int i = 0; i < Cnum; i++)
newCList[i] = CList[i];
// 随机寻找多个个顾客更换其对应的工厂
int count = random(10);
while(count--) {
if (random(10) < 5) {
int c_id = random(Cnum);
int f_id = random(Fnum);
while(newCList[c_id].demand > newFList[f_id].leftCapacity) {
f_id = random(Fnum);
}
newFList[newCList[c_id].belongTo].leftCapacity += newCList[c_id].demand;
newCList[c_id].belongTo = f_id;
newFList[f_id].leftCapacity -= newCList[c_id].demand;
} else {
int c_id1 = random(Cnum);
int c_id2 = random(Cnum);
while(c_id1 == c_id2)
c_id2 = random(Cnum);
if (newCList[c_id1].demand + newFList[newCList[c_id2].belongTo].leftCapacity >= newCList[c_id2].demand &&
newCList[c_id2].demand + newFList[newCList[c_id1].belongTo].leftCapacity >= newCList[c_id1].demand) {
newFList[newCList[c_id1].belongTo].leftCapacity += newCList[c_id1].demand;
newFList[newCList[c_id1].belongTo].leftCapacity -= newCList[c_id2].demand;
newFList[newCList[c_id2].belongTo].leftCapacity += newCList[c_id2].demand;
newFList[newCList[c_id2].belongTo].leftCapacity -= newCList[c_id1].demand;
swap(newCList[c_id1].belongTo, newCList[c_id2].belongTo);
}
}
}
int newSolution = evaluate(newFList, newCList);
if (newSolution < bestSolution) {
bestSolution = newSolution;
swap(CList, newCList);
swap(FList, newFList);
// cout << "当前最好: " << bestSolution << endl;
flag = 0;
}
flag++;
}
return bestSolution;
}
模拟退火算法是对前面局部搜索算法的改进,因为每一次我们都会根据当前最优解产生一个新的解,而如果采用的是局部搜索法,我们会只接受那些优于当前解的解,而对于那些较差的解,我们会将其进行舍弃,这样很容易就陷入了局部最优的策略,所以我们采用模拟退火法,设置一个初温,在开始时,我们容易对较差的解也进行采纳,但随着温度降低,这个接受较差解的概率也逐渐降低,最后也会逐渐收敛。
一、模拟退火算法流程
二、相关邻域操作
这里,我跟局部搜索采取的邻域操作是相同的:
还有初温,末温,降温系数,内循环次数的选取,此处我设定初温为1000,末温为0.00001,降温系数为0.99,内循环次数为1000。
三、模拟退火的优缺点
优点:
缺点:
算法实现的代码
代码地址:https://github.com/chensm9/Algorithm-project/blob/master/SA.cpp
这里主要贴出模拟退火函数部分的代码:
int SArun() {
// initBestSolution();
// 贪心算法得到初始解
Greedyrun();
srand((int)time(0));
int bestSolution = evaluate(FList, CList);
Factility* newFList = new Factility[Fnum];
Customer* newCList = new Customer[Cnum];
double t = T_start;
// 降火初温末温
while (t > T_end) {
// 模拟退火内循环
for (int kk = 0; kk < ILOOP; kk++) {
for (int i = 0; i < Fnum; i++)
newFList[i] = FList[i];
for (int i = 0; i < Cnum; i++)
newCList[i] = CList[i];
// 随机寻找多个个顾客更换其对应的工厂
int count = random(Fnum);
while(count--) {
// 邻域操作
if (random(10) < 5) {
int c_id = random(Cnum);
int f_id = random(Fnum);
while(newCList[c_id].demand > newFList[f_id].leftCapacity) {
f_id = random(Fnum);
}
newFList[newCList[c_id].belongTo].leftCapacity += newCList[c_id].demand;
newCList[c_id].belongTo = f_id;
newFList[f_id].leftCapacity -= newCList[c_id].demand;
} else {
int c_id1 = random(Cnum);
int c_id2 = random(Cnum);
while(c_id1 == c_id2)
c_id2 = random(Cnum);
if (newCList[c_id1].demand + newFList[newCList[c_id2].belongTo].leftCapacity >= newCList[c_id2].demand &&
newCList[c_id2].demand + newFList[newCList[c_id1].belongTo].leftCapacity >= newCList[c_id1].demand) {
newFList[newCList[c_id1].belongTo].leftCapacity += newCList[c_id1].demand;
newFList[newCList[c_id1].belongTo].leftCapacity -= newCList[c_id2].demand;
newFList[newCList[c_id2].belongTo].leftCapacity += newCList[c_id2].demand;
newFList[newCList[c_id2].belongTo].leftCapacity -= newCList[c_id1].demand;
swap(newCList[c_id1].belongTo, newCList[c_id2].belongTo);
}
}
}
int newSolution = evaluate(newFList, newCList);
if (newSolution < bestSolution) {
bestSolution = newSolution;
swap(CList, newCList);
swap(FList, newFList);
// cout << "当前最好: " << bestSolution << endl;
} else {
double dE = newSolution - bestSolution;
double P_k = exp(dE*-1/t);
if (random(1000) * 0.001 < P_k) {
bestSolution = newSolution;
swap(CList, newCList);
swap(FList, newFList);
}
}
}
t *= DELTA;
}
return bestSolution;
}
对于贪心算法,其具体结果数据地址:https://github.com/chensm9/Algorithm-project/blob/master/GreedyResult.txt
以下结果列表只给出如下相应的总费用和运行时间:
测试样例 | 总费用 | 运行时间 |
---|---|---|
p1 | 12063 | 0s |
p2 | 9680 | 0s |
p3 | 12153 | 0s |
p4 | 13923 | 0.015625s |
p5 | 11362 | 0s |
p6 | 9047 | 0s |
p7 | 11004 | 0s |
p8 | 13324 | 0s |
p9 | 10865 | 0s |
p10 | 9557 | 0s |
p11 | 12431 | 0s |
p12 | 14702 | 0s |
p13 | 9898 | 0s |
p14 | 8208 | 0s |
p15 | 11231 | 0s |
p16 | 12831 | 0s |
p17 | 10597 | 0s |
p18 | 8110 | 0s |
p19 | 10998 | 0s |
p20 | 12636 | 0s |
p21 | 9709 | 0s |
p22 | 9024 | 0s |
p23 | 11122 | 0.015625s |
p24 | 12517 | 0s |
p25 | 21009 | 0s |
p26 | 14021 | 0s |
p27 | 18151 | 0.015625s |
p28 | 26261 | 0s |
p29 | 18694 | 0.015625s |
p30 | 16833 | 0s |
p31 | 20009 | 0.015625s |
p32 | 25559 | 0s |
p33 | 18102 | 0.015625s |
p34 | 13541 | 0s |
p35 | 18068 | 0.015625s |
p36 | 22017 | 0.015625s |
p37 | 20064 | 0s |
p38 | 14992 | 0.015625s |
p39 | 20687 | 0s |
p40 | 26858 | 0.015625s |
p41 | 10487 | 0s |
p42 | 8346 | 0s |
p43 | 8387 | 0s |
p44 | 11221 | 0s |
p45 | 8654 | 0s |
p46 | 7888 | 0s |
p47 | 11297 | 0s |
p48 | 8773 | 0s |
p49 | 8802 | 0s |
p50 | 12926 | 0s |
p51 | 10246 | 0s |
p52 | 11970 | 0s |
p53 | 14843 | 0.015625s |
p54 | 14244 | 0s |
p55 | 11719 | 0s |
p56 | 30787 | 0.015625s |
p57 | 39513 | 0.015625s |
p58 | 49313 | 0s |
p59 | 38114 | 0.015625s |
p60 | 31498 | 0.015625s |
p61 | 38350 | 0.015625s |
p62 | 45624 | 0s |
p63 | 39176 | 0.015625s |
p64 | 32363 | 0.015625s |
p65 | 39505 | 0.015625s |
p66 | 43650 | 0s |
p67 | 41075 | 0.015625s |
p68 | 29609 | 0.015625s |
p69 | 38870 | 0s |
p70 | 48981 | 0.015625s |
p71 | 36403 | 0.015625s |
对于局部搜索算法,其具体结果数据地址:https://github.com/chensm9/Algorithm-project/blob/master/HCResult.txt
以下结果列表只给出如下相应的总费用和运行时间:
测试样例 | 总费用 | 运行时间 |
---|---|---|
p1 | 9040 | 0.015625s |
p2 | 7801 | 0.03125s |
p3 | 9666 | 0.015625s |
p4 | 11605 | 0.03125s |
p5 | 9147 | 0.015625s |
p6 | 8343 | 0.015625s |
p7 | 10054 | 0.015625s |
p8 | 11795 | 0.015625s |
p9 | 8938 | 0.03125s |
p10 | 8508 | 0.03125s |
p11 | 10446 | 0.015625s |
p12 | 11602 | 0.015625s |
p13 | 9138 | 0.03125s |
p14 | 7789 | 0.015625s |
p15 | 10100 | 0.046875s |
p16 | 11700 | 0.046875s |
p17 | 9225 | 0.03125s |
p18 | 7252 | 0.015625s |
p19 | 9995 | 0.015625s |
p20 | 11739 | 0.046875s |
p21 | 8736 | 0.015625s |
p22 | 8462 | 0.03125s |
p23 | 10882 | 0.015625s |
p24 | 11808 | 0.015625s |
p25 | 16906 | 0.09375s |
p26 | 11114 | 0.203125s |
p27 | 13495 | 0.140625s |
p28 | 21124 | 0.09375s |
p29 | 13957 | 0.203125s |
p30 | 11713 | 0.234375s |
p31 | 14337 | 0.203125s |
p32 | 16794 | 0.203125s |
p33 | 12535 | 0.15625s |
p34 | 10811 | 0.171875s |
p35 | 13726 | 0.109375s |
p36 | 16767 | 0.125s |
p37 | 14044 | 0.125s |
p38 | 12296 | 0.125s |
p39 | 13697 | 0.28125s |
p40 | 25964 | 0.0625s |
p41 | 7218 | 0.046875s |
p42 | 6536 | 0.03125s |
p43 | 6635 | 0.078125s |
p44 | 8232 | 0.046875s |
p45 | 7183 | 0.078125s |
p46 | 7208 | 0.03125s |
p47 | 7464 | 0.015625s |
p48 | 7126 | 0.0625s |
p49 | 7992 | 0.015625s |
p50 | 10625 | 0.046875s |
p51 | 8003 | 0.046875s |
p52 | 9829 | 0.03125s |
p53 | 10325 | 0.0625s |
p54 | 12012 | 0.09375s |
p55 | 11156 | 0.015625s |
p56 | 25355 | 0.125s |
p57 | 31514 | 0.203125s |
p58 | 43625 | 0.203125s |
p59 | 31445 | 0.203125s |
p60 | 25814 | 0.21875s |
p61 | 30778 | 0.140625s |
p62 | 36780 | 0.1875s |
p63 | 29694 | 0.265625s |
p64 | 24576 | 0.171875s |
p65 | 32732 | 0.1875s |
p66 | 36765 | 0.09375s |
p67 | 30861 | 0.171875s |
p68 | 24010 | 0.21875s |
p69 | 28199 | 0.3125s |
p70 | 40934 | 0.171875s |
p71 | 30342 | 0.140625s |
对于模拟退火算法,其具体结果数据地址:https://github.com/chensm9/Algorithm-project/blob/master/SAResult.txt
以下结果列表只给出如下相应的总费用和运行时间:
测试样例 | 总费用 | 运行时间 |
---|---|---|
p1 | 8917 | 1.29688s |
p2 | 7877 | 1.20312s |
p3 | 9720 | 1.28125s |
p4 | 11249 | 1.15625s |
p5 | 9116 | 1.1875s |
p6 | 7855 | 1.17188s |
p7 | 9855 | 1.15625s |
p8 | 11631 | 1.15625s |
p9 | 8742 | 1.09375s |
p10 | 7855 | 1.21875s |
p11 | 9504 | 1.17188s |
p12 | 10427 | 1.10938s |
p13 | 9342 | 1.78125s |
p14 | 7827 | 1.71875s |
p15 | 9773 | 1.73438s |
p16 | 11981 | 1.71875s |
p17 | 8671 | 1.71875s |
p18 | 7467 | 1.73438s |
p19 | 10142 | 1.75s |
p20 | 11070 | 1.73438s |
p21 | 9311 | 1.6875s |
p22 | 7685 | 1.73438s |
p23 | 9673 | 1.70312s |
p24 | 11470 | 1.875s |
p25 | 14009 | 3.29688s |
p26 | 12453 | 3.25s |
p27 | 14529 | 3.375s |
p28 | 17126 | 3.34375s |
p29 | 14678 | 3.25s |
p30 | 12492 | 3.23438s |
p31 | 15179 | 3.28125s |
p32 | 17931 | 3.42188s |
p33 | 12588 | 3.35938s |
p34 | 11878 | 3.35938s |
p35 | 14489 | 3.26562s |
p36 | 17078 | 3.26562s |
p37 | 13609 | 3.4375s |
p38 | 12175 | 3.26562s |
p39 | 14478 | 3.20312s |
p40 | 16378 | 3.375s |
p41 | 7162 | 1.45312s |
p42 | 7717 | 1.96875s |
p43 | 6943 | 2.54688s |
p44 | 7024 | 1.5s |
p45 | 7302 | 2.1875s |
p46 | 7325 | 2.57812s |
p47 | 6211 | 1.51562s |
p48 | 6477 | 1.98438s |
p49 | 6873 | 2.54688s |
p50 | 9461 | 1.82812s |
p51 | 8784 | 2.1875s |
p52 | 9713 | 1.64062s |
p53 | 10612 | 2.20312s |
p54 | 9756 | 1.67188s |
p55 | 8609 | 2.20312s |
p56 | 23241 | 3.78125s |
p57 | 31920 | 3.67188s |
p58 | 50273 | 3.8125s |
p59 | 36265 | 3.65625s |
p60 | 23413 | 3.71875s |
p61 | 31444 | 3.64062s |
p62 | 50545 | 3.59375s |
p63 | 34530 | 3.6875s |
p64 | 23882 | 3.60938s |
p65 | 31086 | 3.60938s |
p66 | 46717 | 3.85938s |
p67 | 35855 | 3.67188s |
p68 | 23013 | 3.67188s |
p69 | 31760 | 3.625s |
p70 | 43202 | 3.64062s |
p71 | 34707 | 4s |
结果分析:
可以看出而局部搜索因为采用贪心算法的解作为初始解,所以得到的结果都要优于贪心算法的解。
使用贪心算法和局部搜索法的运行时间要远小于使用模拟退火法,因为需要模拟退火需要相应的降温过程,所以其运行时间较长,但是模拟退火法相对局部搜索而言要更加稳定(通过取多次结果对比)。
但是模拟退火得到的结果并不一定就优于另外两个,像最后几个测例,因为可能接受较差解的原因,导致模拟退火的解最后并没有特别优(可能是没有考虑好OpenCost的原因,因为这几个测例的OpenCost较为昂贵)。