算法project实验报告

中山大学数据科学与计算机学院本科生实验报告

(2018年秋季学期)

课程名称 算法设计与分析 任课老师 张子臻
年级 2016级 专业(方向) 计算机应用
学号 16340030 姓名 陈斯敏
电话 15917173057 Email [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.
题目要求给定相应的工厂和顾客,每个工厂有相应的容量(可容纳顾客需求)和相应的开启费用,而每个顾客有相应的需求(固定的)和任务费用(因工厂而改变),现在要对每个工厂进行分配顾客,要求所有顾客都要分配到工厂,然后工厂的容量不能超过它所分配到的顾客的需求之和,同时希望工厂开启费用和顾客任务费用总和最小,每一份数据的输入格式如下:
算法project实验报告_第1张图片


解决方案一、贪心算法

因为题目要求希望求出最优的算法,所以我们很容易可以想到使用贪心算法进行求解,但是贪心策略的选取很关键。对于该问题,贪心算法并不一定能求出最优解法,但是可以在较短的时间求得一个可满足解且该解的优劣程度不会太差。
接下来,讲一下实现该算法的流程和贪心策略:

一、贪心算法流程

  1. 读取数据(采用C++的流读取方式)。
  2. 运用贪心策略选择分配一个未被分配顾客到相对应的工厂(该工厂需要可满足要求)。
  3. 重复第2步的操作,直至分配完所有的顾客。
  4. 评估当前费用,输出结果。

二、贪心策略
采用的贪心选取策略是选取性价比最高的操作,因为需要满足的需求是固定的,而产生的费用是不固定的,所以我们希望性价比最高当然是费用/需求最少,那么,我们每次分配顾客时,便会产生相应的费用,也会满足相应的需求,便可以遍历所有可进行的操作,选取其中费用需求比最小(性价比最高的)的操作,具体公式如下:

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;
 }

解决方案二、局部搜索法(爬山法)

局部搜索法的过程是对一个初始化的解反复进行相关领域操作,直到得到比该解更优的解,然后一直迭代下去,直到逼近最优的解,同样的,该算法也不一定能够得到最优的解,且其解与我们所选取的初始解和进行的邻域操作相关,但因为进行多次迭代能够得到比初始解更优的解,那么我们很容易想到可以将上面贪心算法得到的解作为初始解来进行迭代,那么最后得到的结果一定比贪心算法更优。
一、局部搜索算法流程

  1. 输入数据(同样采用相同的流读取)
  2. 初始化一个最优解(此处可以采用贪心算法的解作为初始解)
  3. 对最优解进行邻域操作,产生一个新的解,如果该解优于当前最优解,则代替当前最优解。
  4. 重复第3步操作,直到N次迭代都未达到更优解,退出重复操作。
  5. 输出当前最优解。

二、相关邻域操作
这里为了效果较好,我采用了两种邻域操作:

  1. 随意寻找一个客户,改变其要分配去的工厂(需要满足容量和需求的关系),以此改变当前的费用总和。
  2. 随意交换两个客户的所属工厂(同样需要满足容量和需求的关系)。

还有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;
 }

解决方案三、模拟退火法

模拟退火算法是对前面局部搜索算法的改进,因为每一次我们都会根据当前最优解产生一个新的解,而如果采用的是局部搜索法,我们会只接受那些优于当前解的解,而对于那些较差的解,我们会将其进行舍弃,这样很容易就陷入了局部最优的策略,所以我们采用模拟退火法,设置一个初温,在开始时,我们容易对较差的解也进行采纳,但随着温度降低,这个接受较差解的概率也逐渐降低,最后也会逐渐收敛。

一、模拟退火算法流程

  1. 输入数据(同样采用相同的流读取)
  2. 初始化一个最优解(此处也可以采用贪心算法的解作为初始解),设置初温,末温,降温系数
  3. 内循环操作,循环一定次数,每次循环对当前最优解进行邻域操作,产生一个新的解,如果该解优于当前最优解,则代替当前最优解。如果该解没有比当前结果更好,则进行温度概率函数筛选判断是否接受
  4. 当前温度乘以降温系数。
  5. 重复第3, 4步操作,当温度降低到末温时,退出重复操作。
  6. 输出当前最优解。

二、相关邻域操作
这里,我跟局部搜索采取的邻域操作是相同的:

  1. 随意寻找一个客户,改变其要分配去的工厂(需要满足容量和需求的关系),以此改变当前的费用总和。
  2. 随意交换两个客户的所属工厂(同样需要满足容量和需求的关系)。

还有初温,末温,降温系数,内循环次数的选取,此处我设定初温为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较为昂贵)。

你可能感兴趣的:(算法题目)