求解VRP问题的启发式算法主要有两大类,一类主要是1960至1990之间提出的经典启发式算法,另一类是1990年之后发展起来的元启发式算法。本章主要讲解求解VRP的经典启发式算法,元启发式代发将在后一章进行讲解。
5.1 节约里程法
5.1.1 C-W节约算法简介
C-W节约算法是有Clarke和Wright在1964年提出的一种求解经典CVRP的启发式算法,作为相对较早提出的一种启发式算法,虽然C-W节约算法一般情况下不能获取CVRP的最优解,但是却可以很容易的获取相对比较好的可行解,而且求解思路简单明了。
C-W算法进行成本节约的逻辑可以从图1中两种运输方案的对比来体现。
图2 C-W节约算法的基本逻辑
图1(a)中安排了两辆车进行货物的运输,即一辆车从0将i所需的物品运送至i后再返回0,另一辆车从0将j所需的物品运送至j后再返回0;图1(b)中安排一辆车将i和j所需的物品一次性运送至i和j,即该辆车先从0将i和j所需的物品全部装上车,然后先行驶至i处将i所需的物品卸载下来,然后继续行驶至j处将j所需的物品卸载下来,最后返回0。另为从节点i至节点j的运输成本(或运输里程),为(a)图的运输成本,为(b)图的运输成本,为(b)图方案要比(a)图方案节约运输成本(里程),则有下式成立:
基于三角不等式原理,只要将两个节点分别运输组合成一次运输,节约量必然大于零。当物品配送的客户数量较多,地理分布没有特定规律时,如何根据车辆容量将多个节点的运输集中到一辆车运输,从而最大程度的节约运输里程,C-W算法另一个需要解决的任务就是进行运输任务合并从而实现运输里程的最大化节约。
C-W算法中里程节约的合并过程分为两种:序贯节约里程法和并行节约里程法,序贯合并过程中每次运算中只进行一条路径的合并,而并行合并每次运算中可以允许多条路径的合并。
不论哪种合并程序,该算法的前期步骤都是一样的,下面以具体数值来说明。
- 给出任意节点对之间的距离,给出所有需求节点的需求量和车辆容量数据;
表5.1 节点间相互距离
0 | 1 | 2 | 3 | 4 | 5 | 6 | |
0 | 30 | 65 | 67 | 53 | 54 | 28 | |
1 | 43 | 72 | 50 | 74 | 53 | ||
2 | 52 | 27 | 89 | 65 | |||
3 | 20 | 49 | 40 | ||||
4 | 60 | 43 | |||||
5 | 15 | ||||||
6 |
表中节点0为配送中心,其他节点为需求点。6个需求节点的需求量依次为:28、35、30、40、45、25,车辆容量为100。
- 计算任意两个需求节点对合并共同运输所能节约的里程;
表5.2 两点合并的里程节约
| 1 | 2 | 3 | 4 | 5 | 6 |
1 | 52 | 25 | 33 | 10 | 5 | |
2 | 80 | 91 | 30 | 28 | ||
3 | 100 | 72 | 55 | |||
4 | 47 | 38 | ||||
5 | 67 | |||||
6 |
以表2中第一行第二列的数据52为例做简要说明,该数据(结合图2)表示如果将派两辆车分别给需求点1和需求点2送货改为使用一辆车给需求点1和需求点2送货车辆运行距离的节约辆,即:如果两辆车分别送货,车辆运行距离为:=30+30+65+65=190;如果一辆车集中送货,车辆运行距离为:=30+43+65=138;节约里程为:190-138=65。
- 将需求节点节约里程按照降序排列
表5.3 示例节约里程表
行号 | 节点对 | 节约量 |
1 | 3-4: | 100 |
2 | 2-4: | 91 |
3 | 2-3: | 80 |
4 | 3-5: | 63 |
5 | 5-6: | 67 |
6 | 3-6: | 55 |
7 | 1-2: | 52 |
8 | 4-5: | 47 |
9 | 4-6: | 38 |
10 | 1-4: | 33 |
11 | 2-5: | 30 |
12 | 2-6: | 28 |
13 | 1-3: | 25 |
14 | 1-5: | 10 |
15 | 1-6: | 5 |
节点对的数量应该有个。
5.1.2 C-W节约算法程序实现基本流程
(1)序贯节约里程法
序贯节约里程法是指按顺序逐步生成全部路径,即每次搜索只生成一条路径。以上面的算例为例,说明序贯节约里程法的一般步骤:
Step1:安排第一辆车的路径。首先考虑节点3和4能不能合并到一条路径,生成0-3-4-0的路径,由节点3和节点4的需求量分别为30和40,可知需求总量70,可以由一辆车运输,因而先生成路径0-3-4-0;然后再看节约里程表中第二行为2-4,由于节点4在已经生成的路径中,因此考虑节点2和节点4能不能合并,即路径0-3-4-2-0是否可行,节点2的需求量为35,同节点3和4的需求量汇总起来共105,大于车辆的容量,因而路径0-3-4-2-0不可行;再看节约里程表中第三行为2-3,同样容量超限,所以这一行忽略;再看节约里程表第4行3-5,同样容量超限,所以这一行忽略;再看节约里程表第5行5-6,这两个节点都没有出现在当前的路径中,而序贯节约里程法一次只生成一条路径,因此这一行暂时也忽略;再看节约里程表第6行为3-6,考虑节点6和3能不能合并,即路径0-3-4-2-0是否可行,因为节点6的需求量为25,可以加入路径,从而生成路径0-6-3-4-0;依次类推,可以看出最后生成第一辆车的路径为0-6-3-4-0。根据该路径将节约里程表中所有同3、4、6的行删除后,获得新的节约里程表如下:
行号 | 节点对 | 节约量 |
1 | 1-2: | 52 |
2 | 2-5: | 30 |
3 | 1-5: | 10 |
Step2:安排第二辆车的路径。首先考虑节点1和2能不能合并到一条路径,生成0-1-2-0的路径,由节点1和节点2的需求量分别为28和35可知需求总量63,可以由一辆车运输,因而生成路径0-1-2-0;然后再看节约里程表中第二行为2-5,考虑节点2和节点5能不能合并,即考察路径0-1-2-5-0是否可行,节点5的需求量为45,同节点5和6的需求量汇总起来共108,超出车辆容量,因而路径0-1-2-5-0不可行;再看节约里程表中第三行1-5,考虑节点1和节点5能不能合并,同样容量超限;因此第二辆车的路径为:0-1-2-0。将节约里程表中涉及节点1和节点2的行全删除,节约里程表结束。
Step3:查看已安排路径,发现节点5还没有安排车辆,最后第三辆车的路径为:0-5-0。全部节点安排车辆结束。
通过序贯节约里程法可得三条路径:0-6-3-4-0,0-1-2-0,0-5-0,三条路径的运行距离分别为:141、138、108,总运行距离为385。
(2)并行节约里程法
并行节约里程法是指在一次搜索中并行生成全部路径。以上面的算例为例,说明并行节约里程法一次搜索步骤中的处理逻辑:
Step1:查看节约里程表第一行为3-4,考虑节点3和4能不能合并到一条路径,生成0-3-4-0的路径,由节点3和节点4的需求量分别为30和40,可知需求总量70,可以由一辆车运输,因而生成路径0-3-4-0(路径A);
Step2:再看节约里程表中第二行为2-4,由于节点4在已经生成的路径中,因此考虑节点2和节点4能不能合并,即路径0-3-4-2-0是否可行,节点2的需求量为35,同节点3和4的需求量汇总起来共105,大于车辆的容量,因而路径0-3-4-2-0不可行;再看节约里程表中第三行为2-3,同样容量超限,所以这一行忽略;再看节约里程表第4行3-5,同样容量超限,所以这一行忽略;
Step3:再看节约里程表第5行5-6,这两个节点都没有出现在当前的路径中,并行节约里程法一次需要生成全部路径,因此考虑节点5-6能不能合并到一条路径,生成0-5-6-0,由于节点5和节点6的需求量分别为:45、25,可知需求总量70,可以由一辆车运输,因此生成路径0-5-6-0(路径B);
Step4:再看节约里程表第6行为3-6,这两个节点分别在路径A和路径B中,因此判断两条路径能不能生成路径:0-5-6-3-4-0。由于路径A需求总量为70,路径B需求总量也为70,两条路径总量140超出车辆的容量,所以这一行忽略;
Step5:再看节约里程表第7行为1-2,这两个节点都没有出现在当前的路径中,并行节约里程法一次需要生成全部路径,因此考虑节点1-2能不能合并到一条路径,生成0-1-2-0,由于节点1和节点2的需求量分别为:28、35,可知需求总量63,可以由一辆车运输,因此生成路径0-1-2-0(路径C);
Step6:再看节约里程表第8行为4-5,这两个节点分别在路径A和路径B中,根据Step4可知两条路径无法合并;
Step7:依次搜索节约里程表至结束,发现没有可能将已知的三条路径再进一步合并,同时全部节点都已经安排的路径,所以搜索结束。
并行搜索算法生成了三条路径:0-3-4-0、0-5-6-0、0-1-2-0,三条路径的运行距离分别为:140、97、138,总运行距离为375。可以看出并行节约里程法生成的路径在运行距离要比序贯节约里程法短。
5.1.3 C-W节约算法java程序
C-W节约里程法两种实现程序分别集成在easyopt.rar包中的类VRP文件中的两个方法:optBySavingCWSeq和optBySavingCWParallel,根据用户输入的节点坐标xyPosition、车辆容量truckCap和节点需求量xyDmd,生成相对运输路径较短的车辆路由方案。
optBySavingCWSeq序贯算法伪代码:
输入问题参数 计算节约里程表A 路径集合S=∅ while(A不为空) 生成一条路径R,将车场序号0加入路径 for(i=1 to A.length){ 判断A中当前行的两个节点是否有至少一个能加入R的 if(能){ 将A中当前行A[i]的两个或一个节点加入R 将A[i]删除 } } for(i=1 to A.length){ 判断A中当前行的两个节点是否有至少一个在R中 if(有){ 将A[i]行删除 } } 将车场序号0加入路径R最后 S=S+R end 对于没有安排进S的节点i,每个生成一条路径R={0,i,0} 输出S及其总里程 |
optBySavingCWParallel序贯算法伪代码:
输入问题参数 计算节约里程表A 路径集合S=∅ for(i=1 to A.length) inRouteQty=A中当前行A[i]两个节点在S中各条路径中的数量 if(inRouteQty==0){ 判断A中当前行的两个节点[p1,p2]能否合并到一条路径 if(能){ 新建一条路径R={0,p1,p2},并将其加入S } elseif(inRouteQty==1){ 判断A[i]中不包含在S中的节点能否合并到另一个节点的路径 if(能){ 将A[i]中不包含在S中的节点合并到另一个节点所在路径 } } end 将S中的路径都增加节点0收尾 对于没有安排进S的节点i,每个生成一条路径R={0,i,0} 输出S及其总里程 |
5.2 改进节约里程法
5.2.1 算法概述
节约里程法生成了一个近似最优解之后无法对其进行改进,为了解决这个缺点,Gaskell和Yellow提出了一种改进节约里程法,该方法使用下式判定两个路径合并节约的里程:
式中是路径形状参数(route shape parameter),该参数越大,准备合并的两个顶点之间的路径长度在表达式中占的权重越大。Golden等研究了当为0.4或1时可以生成较好的结果。
5.3.2 算法实验
使用easyopt.jar包中的VRPinstance类和VRP类进行算法性能测试。
(1)单一算例实验
每次只进行单个算例进行算法性能测试,测试主程序如下:
import easyopt.instances.VRPinstance; import java.util.ArrayList; /**用于测试使用VRP类中的节约里程法和改进型节约里程法求解CVRP的性能 * */ public class TestSavingAlgorithm { public static void main(String[] args) { VRPinstance instance = new VRPinstance(); instance.initEn22K4();//使用VRPinstance类中的En22K4算例数据进行测试 int truckCap = instance.capacity; int[] dmd = instance.dmdQty; double[][] posXY = instance.posXY; System.out.println("***************already optimal result: "+ instance.optResult); double sumCost =0; ArrayList<VRP.Route> rList = VRP.optBySavingCWSeq(truckCap, dmd, posXY); for(VRP.Route r:rList){ sumCost+=r.sumCost; } System.out.println(" saving.....sequence "+sumCost); double sumCost2 =0; ArrayList<VRP.Route> rList2 = VRP.optBySavingCWParallel(truckCap, dmd, posXY); for(VRP.Route r:rList2){ sumCost2+=r.sumCost; } System.out.println(" saving.....parallel " + sumCost2); // System.out.println(rList2.toString()); sumCost =0; double lamda=0.6; rList = VRP.optByModSavingSeq(truckCap, dmd, posXY,lamda); for(VRP.Route r:rList){ sumCost+=r.sumCost; } System.out.println(" modified saving.....sequence "+sumCost); sumCost2 =0; rList2 = VRP.optByModSavingParallel(truckCap, dmd, posXY,lamda); for(VRP.Route r:rList2){ sumCost2+=r.sumCost; } System.out.println(" modified saving.....parallel "+ sumCost2); } } |
执行结果如下:
***************already optimal result: 375.0 saving.....sequence 458.0 saving.....parallel 387.0 modified saving.....sequence 448.0 modified saving.....parallel 399.0 |
(2)多次实验
以En22K4、En30K3、En51K5、En76K10和En101K8五个算例为运算数据,λ分别设为0.2、0.4、0.6、0.8四个数值,进行节约里程法和改进节约里程法优化实验,实验程序如下:
import easyopt.common.EasyArray; import easyopt.instances.VRPinstance; import java.util.ArrayList; /**用于测试使用VRP类中的节约里程法和改进型节约里程法求解CVRP的性能 * */ public class TestSavingAlgorithm { public static void main(String[] args) { manyExperiment(); } public static void manyExperiment(){ double[][] results = new double[5][10]; for(int i=0;i<5;i++){ VRPinstance instance = new VRPinstance(); if(i==0) instance.initEn22K4(); if(i==1) instance.initEn30K3(); if(i==2) instance.initEn51K5(); if(i==3) instance.initEn76K10(); if(i==4) instance.initEn101K8(); int truckCap = instance.capacity; int[] dmd = instance.dmdQty; double[][] posXY = instance.posXY; double sumCost =0; ArrayList<VRP.Route> rList = VRP.optBySavingCWSeq(truckCap, dmd, posXY); for(VRP.Route r:rList){ sumCost+=r.sumCost; } results[i][0] = sumCost; double sumCost2 =0; ArrayList<VRP.Route> rList2 = VRP.optBySavingCWParallel(truckCap, dmd, posXY); for(VRP.Route r:rList2){ sumCost2+=r.sumCost; } results[i][1] = sumCost2; for(int j=0;j<4;j++){ sumCost =0; double lamda=0.2+0.2*j; rList = VRP.optByModSavingSeq(truckCap, dmd, posXY,lamda); for(VRP.Route r:rList){ sumCost+=r.sumCost; } results[i][2 + 2*j ] = sumCost; sumCost2 =0; rList2 = VRP.optByModSavingParallel(truckCap, dmd, posXY,lamda); for(VRP.Route r:rList2){ sumCost2+=r.sumCost; } results[i][2 + 2*j + 1 ] = sumCost2; } } EasyArray.printArray(results,0); } } |
实验结果列于表5.4,其中SeqCW、PrlCW、MseqCW和MPrlCW分别为序贯节约里程法、并行节约里程法、改进序贯节约里程法和改进并行节约里程法。
表5.4 节约里程法和改进节约里程法实验结果表
算例 | SeqCW | PrlCW | λ=0.2 | λ=0.4 | λ=0.6 | λ=0.8 | ||||
MSeqCW | MPrlCW | MSeqCW | MPrlCW | MSeqCW | MPrlCW | MSeqCW | MPrlCW | |||
En22K4 | 458 | 387 | 518 | 469 | 427 | 444 | 448 | 399 | 458 | 387 |
En30K3 | 817 | 586 | 969 | 599 | 754 | 571 | 783 | 594 | 812 | 594 |
En51K5 | 734 | 591 | 1045 | 595 | 824 | 568 | 720 | 579 | 689 | 579 |
En76K10 | 1061 | 907 | 1355 | 1014 | 1143 | 931 | 1078 | 897 | 1066 | 878 |
En101K8 | 1197 | 999 | 1633 | 1041 | 1299 | 959 | 1275 | 940 | 1277 | 938 |
从表5.4可以看出,并行算法通常要优于序贯算法,对于改进节约里程法求解结果受到参数λ的影响,且当参数λ在某些取值时可以获得比普通节约里程法要好的结果。
5.3 扫描算法(Sweep Algorithm)
5.3.1 算法概述
扫描算法(Sweep Algorithm)是以车场为极点进行扇形扫描的方式对客户进行分组,每一组客户需求由一辆车负责服务,组中的具体路径使用TSP相关算法进行优化求解以获得每一组的最短路径。该算法的基本思想是极角和极径相近的客户安排到同一辆车有较大可能获得最小的总里程。扫描算法开始由Wren在1971年最先提出,后来由Gillett在1974年对其进行具体描述和寻优性能分析,使其广为人知。
当给出车场和各个节点的二维平面坐标之后,可以算出按照下式算出各个节点i的极坐标:
车场二维平面坐标为,节点i的二维平面坐标为。节点i的极角在[-π,+π]之间,极径为该点与车场之间的欧几里得距离。VRP问题中节点极角和极径示意图如图1所示。
图1 扫描算法中确定客户节点极坐标示意图
扫描算法基本步骤如下:
Step1(初始化路径):选择一个还没有使用的车辆k。
Step2(路径构建):对未分配车辆的客户节点按照极角由小到大分配给车辆k,直至车辆k相关约束条件不满足时为止,然后对车辆k的具体行驶路径进行优化,获得一辆车的具体路径安排。如果还有节点未安排路径,则继续执行Step1,直至全部客户被安排了车辆。
Step3(坐标旋转):将X和Y坐标互换,即重新计算极角,然后重复Step1和Step2,获得多组解。这一步本质上是重新选择和设定极角最小的节点,然后以该节点为起点,逐步向极角大的节点扫描,分配车辆。
Step4(逆排序求解):重复Step2和Step3,只是在Step2中将各个客户节点按照极角由大到小排序,然后进行车辆分配,获得问题的解。
Step5(近似最优解输出):从上述求解结果中找出最好的解进行输出。
下面使用示例说明sweep算法的基本步骤。
例如在一个CVRP问题中,有18个客户需要配送中心进行送货。客户根据各自极坐标由小到大排序后进行编号,各自的需求量依次为:14、15、13、18、4、19、8、13、7、18、21、11、14、5、23、16、17、21,车辆容量50,试采用扫描算法的步骤来获得车辆路径方案。
分别以节点1、3和9作为起始节点进行扫描分配,分配结果列入表1、表2和表3,相同颜色的客户将由同一辆车进行服务。表中j行数字为所用车辆的序号,例如表1中节点1、2和3列最下面的1表示这三个节点由车辆1服务。从表中可以看出,三种方案下单辆车所服务的节点分配可能存在不同,最后全部车辆行驶总里程必然也可能存在差异。
表1: 以节点1为起点进行任务分配结果示意图
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
Qi | 14 | 15 | 13 | 18 | 4 | 19 | 8 | 13 | 7 | 18 | 21 | 11 | 14 | 5 | 23 | 16 | 17 | 21 |
j | 1 | 2 | 3 | 4 | 5 | 6 | ||||||||||||
表2: 以节点3为起点进行任务分配结果示意图 | ||||||||||||||||||
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
Qi | 14 | 15 | 13 | 18 | 4 | 19 | 8 | 13 | 7 | 18 | 21 | 11 | 14 | 5 | 23 | 16 | 17 | 21 |
j | 6 | 1 | 2 | 3 | 4 | 5 | 6 | |||||||||||
表3: 以节点9为起点进行任务分配结果示意图 | ||||||||||||||||||
i | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
Qi | 14 | 15 | 13 | 18 | 4 | 19 | 8 | 13 | 7 | 18 | 21 | 11 | 14 | 5 | 23 | 16 | 17 | 21 |
j | 5 | 6 | 7 | 1 | 2 | 3 | 4 |
5.3.2 Sweep算法流程
Gillet在使用Sweep算法研究的VRP问题为具有车辆里程限制的CVRP问题,即每辆车一个回路行驶的总里程不能超过D,该问题隐含问题参数需要满足如下约束:
- 每个客户节点需求量不能超过车辆容量C;
- 每个客户节点距离车场的距离不能超过D/2。
Gillet扫描算法在扫描过程中确定了当前车辆路径安排之后,还考虑是否能够将其同下一条路径之间进行节点互换,实现路径的进一步优化,其算法步骤如下为:
Step1:输入各个节点的二维平面坐标集合X和Y,车场编号为0、后续客户编号为{1,2,...,n},各个节点的需求量集合Q,车辆容量C,车辆单条路径最长距离阈值为D,节点之间的距离矩阵A。
Step2: 根据节点i坐标集合X[i]和Y[i],按照下式求出该节点的极坐标:
其中每个节点极角在[-π,+π]之间。
Step3:将各个客户节点(非0节点)按照极角由小到大排序,如果两个节点极角相同,则按照极径由小到大再排序,这样可以形成全部客户节点的一个唯一排序,假设全部客户节点排序后的序号分别为1、2、3、...、n,存在集合K中,即K={1,2,...,n}。
Step4:令待分配客户索引号i=1,进行正向扫描,详见图2:
Step4.1:构建一条新路径R,将K[i]客户加入该路径,当前路径载运量LQ = Q[K[i]]。
Step4.2:令i=i+1。
Step4.3: 如果 LQ + Q[K[i]]>C或i=n, 执行Step4.5;否则执行Step4.4。
Step4.4: 将节点K[i]加入当前路径,并更新LQ = LQ + Q[K[i]],执行Step4.2,继续向当前路径添加客户。
Step4.5: 此时当前路径R中所包含的最后一个节点索引号为i-1,集合K中的第i个节点不能加入到当前路径,或者装载量将超过车辆容量,或者第i个节点为最后一个节点。当i为最后一个节点时,执行Step4.13;利用Lin的3opt算法求解当前路径R的最短里程D1,如果最短里程超过车辆里程约束,将K[i-1]从路径R中删除,更新LQ = LQ- Q[K[i-1]],i=i-1,继续执行本步骤,直至当前路径所包含的节点需求满足车辆单程距离约束,然后执行下一步。
Step4.6:此时当前路径R中包含的客户节点总需求量不超过车辆容量,且根据Lin的3opt算法对这些客户路径进行优化后满足车辆里程约束,路径R中最后加入的客户节点在K中的索引号为i-1。本步骤是尝试将路径R中最靠近车场和下一个扇区的客户同尚未分配路径的客户来替代,以改善路径安排。具体方法是按照表达式找出路径R中待换出的客户索引号OutId(OI),该表达式含义是当客户越靠近车场被换出的概率越大、客户越靠近下一个扇区被换出的概率也越大;再将待分配客户中与R中最后一个客户距离最近的客户索引号找出来InId(II),同时将待分配客户中与K[II]距离最近的客户索引号也找出来InId2(II2)。
图2 sweep算法路径优化示意图
该步骤实现路径优化的基本思想可以用图2来表示,R为当前正在构建的路径,R1为R之后可能构成的路径,但是这个路径由于不知道究竟会包含多少个客户节点,所以算法中只考虑当前路径之后的六个节点。图中第12个客户节点为待换出节点,它在路径R中满足寻找OI的表达式,第16个节点是距离R中最后一个顾客节点LN(Last Node)最近的未安排客户节点II,第18节点是距离II最近的未排客户节点II2。
假设原有路径R使用Lin3opt得到最优里程为D1,R1的最优里程为D3,如果将节点12和16互换后,新R成员为{9,10,11,13,16},新R'成员为{12,14,15, 17,18,19},此时新R最优里程为D2,新R1最优里程为D4,如果D1+D3<D2+D4,就表示互换后前后两条路径总里程有改善,则进行互换;否则继续考虑将16和18同时与12互换,则新R成员为{9,10,11,13,16,18},新R1成员为{12,14,15, 17,19},此时新R最优里程为D5,新R1最优里程为D6,然后判断表达式D1+D3<D5+D6是否成立,如果成立表示12和待排的16和18两个互换能够优化前后两条路的总里程,则进行互换。
Step4.7:将K[II]加入路径R,将K[OI]从路径R中剔除,如果新路径装载量满足车辆容量约束,计算新路径的最短里程为D2,且该里程满足里程约束,执行Step4.9;否则执行Step4.8。
Step4.8: 路径R构建完毕,将其加入解集S,执行Step4.1。
Step4.9: 判断[II]是否存在于接下来路径中的前五个节点中,如果不存在,执行Step4.8;否则计算从车场0出发,经过节点K[i]、K[i+1]、K[i+2]、K[i+3]、K[i+4]并最后到达K[i+5]的路径R1的最短里程为D3;将集合K中的[OI]和[II]互换,计算从车场0出发,经过节点K[i]、K[i+1]、K[i+2]、K[i+3]、K[i+4]并最后到达K[i+5]的路径的最短里程为D4;如果D1+D3<D2+D4,执行Step4.11;否则执行Step4.10。
注:D3和D4计算的路径都是链状的路径,不是环状路径,都是从车场0出发,然后到K[i+5]结束。
Step4.10:将节点II放入路径,将节点OI移出路径,执行Step4.2。
Step4.11: 判断[II]和[II2]是否在接下来路径中的前五个节点中,如果不存在,执行Step4.8;否则计算从车场0出发,经过节点K[i]、K[i+1]、K[i+2]、K[i+3]、K[i+4]并最后到达K[i+5]的路径R1的最短里程为D5;将集合K中的[OI]和[II]与[II2]两个节点互换,计算从车场0出发,经过节点K[i]、K[i+1]、K[i+2]、K[i+3]、K[i+4]并最后到达K[i+5]的路径的最短里程为D6;如果D1+D3<D5+D6,执行Step4.8;否则执行Step4.12。
Step4.12:将[II]和[II2]换入路径R,将[OI]换出路径R,并执行Step4.2。
Step4.13:此步骤判断将最后一个节点加入当前路径后是否能够满足距离约束,如果满足距离约束,则算法结束;否则执行Step4.14。
Step4.14:判断将最后一个节点与R中的OI节点互换是否有改善,如果无改善则将R加入最后的解集,并将最后一个节点单独作为一条路径加入最后解集,算法结束;如果有改善,则将最后一个节点与OI互换,K[i]加入R,将R加入最后解集S,[OI]作为单独一条路径加入最后解集S,算法结束。
Step5:选择K中不同节点作为起点,实施Step4,形成新的解集。
Step6:将Step4.2中的i=i+1改为i=i-1,进行逆序扫描,获得解集。
Step7:选择上述解集中的最好解输出,算法整体结束。
图2 Sweep算法单次循环运算流程图
通过上面示例和扫描算法基本运算流程可以看出扫描算法具有如下特征:
- 从不同节点出发进行一个方向按照极角大小进行扫描所形成的不同方案最后的总里程可能不相同;
- 按照顺时针进行节点车辆分配和按照逆时针进行节点车辆分配所形成的方案可能不同,总里程也可能不同;
- 所形成的相邻路径之间进行节点互换,有可能会改善目标值。
参考文献:
[1] A.Wren. Compters in Transport Planning and Operation. Ian Allan, London,1971.
[2] B.E. Gillett and L.R. Miller. A heuristic algorithm for the vehicle dispatch problem. Operaations Research, 1974,22:340-349.
5.3.3 最简Sweep算法java编程实现
从上一小节中Sweep的算法流程描述中可以看出,该算法的基本思想虽然很简单,但是进行路径中节点互换的优化改善逻辑比较复杂。本节仅考虑Sweep中扫描过程实现路径规划,不考虑路径之间节点互换的优化过程,编程实现该算法程序,初步评估一下算法的性能。假设问题中客户节点数量为n,编号依次用1、2、...、n表示,车场用0表示,最简sweep算法的运算流程如下:
Step1:根据每个客户和车场的二维坐标,获得每个客户的极坐标 Step2:对n个客户按照极角和极径升序排列,获得排序集合K Step3:分别以K中的第1、2、...、n客户为起始扫描点,向后扫描每个客户,形成客户车辆分配方案,使用Lin3opt计算方案总里程; Step4:再分别以K中的第n、n-1、n-2、...、2和1客户为起始扫描点,逆序扫描每个客户,形成客户车辆分配方案,使用Lin3opt计算方案总里程; Step5:比较Step3和Step4中形成的2n个路径方案,选择最优的作为近似最优解输出。 |
以一个客户为起点扫描获得路径方案的伪代码如下:
客户在K中的序号为i S={},车辆路径集合 R={K[i]},将K[i]客户加入路径,LQ=Q[K[i]] j=1; while(j<=n){ m = mod(i+j,n),获得本次拟加入路径R的客户在K中的索引号 if(LQ + Q [K[m]]<=C){//满足容量约束 R+={K[m]}, LQ += Q [K[m]],即将位置m对应的客户加入路径 j++
}else{//第m个客户不能加入路径,否则超出了容量约束 needChecked = true while(needChecked){//进行里程约束的判断 d = f(R):使用Lin3opt获得R的最短里程 if(d<=D){ needChecked = false; RT+={R} m = mod(i+j,n) R={K[m]},LQ = Q[K[m]], 即R重置,并将第m个客户信息赋给R j++ }else{ j— R中去除最后一个客户 } } } } |
算法计算结果同节约里程法和改进节约里程法相比,寻优效果较差,该算法不能使用这种简单的形式来实施,还是需要进行改善操作。
5.3.4 Sweep算法伪代码
包含改善过程的Sweep算法伪代码:
参数初始化 计算和排序 boolean solutionNotFinish=true int i=1; while(i<n) 定义新路径R,将K[i]加入R,计算装载量LQ boolean routeNotFinish = true while2(routeNotFinish) i=i+1; if(LQ+Q[K[i]]>C){ 求R的最小里程D1 while(D1>D){ i=i-1 更新R和D1 } R满足条件 计算出待换出节点OI,待换入节点II,II2 if(OI换II后容量超限){ status =2;//容量超限1 }else{ if(OI换II后距离超限){ status=3;//距离超限1 }else{ status=1; } } if(status==3){ if(OI换II和II2容量超限){ status==4 }else{ if(OI换II和II2距离超限){ status=5 }else{ status =6; } } } //下面根据status的值,进行对应的处理 switch status case 0:{进行OI和II互换,i=i-1,对冲while2循环后的i=i+1} case 1,3,4:{直接输出R,routeNotFinish=false} case 5:{进行OI和II和II2互换,i=i-1,对冲while2循环后的i=i+1} }else{ R=R+K[i] } endwhile2 endwhile 判断第n个节点是能够加入到最后一条路径,还是能够同最后一条路径互换? |
5.4 λ互换下降法
5.3.1 算法概述
前述算法中节约里程法属于构造法求解,即每次构造一条或多条路径,最后形成了问题的解;扫描算法属于先对客户进行分组,然后对组内客户路径进行优化,进而最后形成问题的解。这两种方法形成的VRP问题解可能还存在着改善空间,Osman等1993年提出了可以进行路径之间节点的交换来改善解质量,并设计了详细的求解步骤。
Osman等将其算法称为λ互换下降法,基本思想是不断对路径对之间进行λ个节点互换,如果互换后形成的解质量有所提升则保留,然后重复该过程,直至解质量无法改善为止。
5.3.2 λ互换机制
假设VRP问题有一个可行解为,其中K为所用车辆数量,也就是路径的数量,为路径或车辆包含的顾客集合。λ互换下降法需要通过λ互换方式来生成当前解S的邻域解集,具体互换方式是选择S中的一对互换路径,例如和,然后将两条路径中的子集且和且进行互换,形成两条新的路径和以及新解,当然两条新路径必须要满足车辆容量等约束条件。通过对全部路径对及其之间的子集互换形成了众多的新解,这些新解组成当前解的邻域解集,为了提高运算速度,一般λ取值为1或2。
对于有K条路径的解来说,将有K(K+1)/2个路径对,搜索过程将依次对这些路径对进行节点互换和判断。当选定了一对路径之后,将根据λ的数值按照特定方式进行互换。下面以λ=1为例说明节点互换过程,λ=2时的互换过程与之类似。
- 挪动操作:将一个路径中的一个节点移动到另一个路径中,而不进行相互交换。挪动变换以操作符(1,0)和(0,1)分别两种操作过程,(1,0)表示从前一个路径中挪动一个节点到后一个路径,(0,1)正好相反。图3表示路径对(R2,R3)中进行了(1,0)的挪移操作,从R2中挪移了一个节点4到R3中,这之后R2为空路径,R3={5,4,6,7,8}。
(a) (1,0)操作前 (b) (1,0)操作后
图3 挪移操作示意图
- 互换操作:分别从两个路径中各取一个进行交换,使用操作符(1,1)来表示。图4表示路径对(R2,R3)中进行了(1,1)的互换操作,即将R2中的节点4和R3中的节点6进行了互换,形成了新的R2和R3。
(a) (1,1)操作前 (b) (1,1)操作后
图4 互换操作示意图
对于任意一对路径,在进行互换操作时将对其全部可能互换的节点依次进行(1,0)、(0,1)和(1,1)操作,然后对每个操作效应进行评价。
5.3.3 操作效应评价
将解S变换为其邻域解集中的解的过程称之为操作,两个解目标函数的差值使用表示,称为该操作的效应。由于λ互换下降法中解S和S'对比,只有互换的两个路径总里程发生变化,其他路径里程没有变化,所以操作效应可由下式获得:
(1)
其中和在变换之前已经计算出来了,所以只需要计算两个互换节点之后路径的里程和即可获得的值。考虑到计算单条路径最优里程是一个TSP问题,当节点数量较多时运算耗时非常大,λ互换下降法先采取两种简单的近似算法来对其进行评价,然后再进行,然后将之与
(1)插入/删除近似评价法
令为所组成的TSP问题的路径排列,该路径里程为,令i为即将互换插入/删除该路径的一个客户节点,而且该节点会插入/删除路径排列中紧邻的两个客户节点r和s之间,形成新排列、新路径集合。
节点i插入/删除前后两个路径的里程差值为,将i在中全部可能位置下的里程差值中最小值作为该路径的近似里程值:
则通过互换后的新路径的近似里程为:
当路径插入节点为+号,当路径删除节点为-号。
(2)3-opt评价法
Lin设计的3-opt算法是一种较好的求解TSP问题启发式算法,可以通过一条初始可行路径中3条边的互换而不断改善解的质量,最后获得近似最优解。当一个操作通过插入/删除操作可以获得路径里程节约的效应时,再使用3-opt算法对该路径进行进一步的优化,从而获得该路径的近似最优解,然后再利用式(1)判定操作的效应。
5.3.4 可行解选择策略
λ互换下降法通过互换机制和操作效应评价运算,获得了当前解S的邻域解集,通常可以采用如下两种策略从邻域解集中选择一个可行解替代当前解以继续迭代运算,实现解的改善和寻优。
(1)最优改善选择策略(Best-improve,BI):从邻域解集中选择改善效果最好的可行解替换当前解;
(2)最先改善选择策略(First-improve,FI):以互换操作过程中最先出现改善的可行解替换当前解。
这两种策略影响了算法运行的流程,当选择BI策略时,每次迭代过程会将全部邻域解都生成之后再进行解的替换判断;当选择FI策略时,每次迭代过程中产生一个邻域解则会进行一次替换判断,如果新生成的邻域解有改善,则不再进行后续的邻域解搜索,直接进行下一次迭代操作。
结合参数λ数值和可行解选择策略,互换下降法可以分为1+BI、1+FI、2+BI和2+FI四种算法,分别表示λ参数为1或2,选择策略为最优改善选择策略或最先改善选择策略。这四种算法对于特定的问题运算性能可能存在差异,可以通过算例实验来进行比较和判断。
5.3.5 算法流程
λ互换下降法是一种迭代改善的启发式算法,它从一个初始可行解出发,通过不断互换操作来改善可行解,并最后获得一个近似最优解。初始可行解可以使用随机生成的方式产生,也可以使用其他运算速度快的启发式算法来生成,例如节约里程法。
BI策略下的λ互换下降法运算流程如下:
Step1:通过特定方式生成初始可行解S; Step2:通过λ互换机制生成S的邻域解集; Step3:从选择效应值最小的解作为新解S'; Step4:如果,令S=S',继续执行Step2;否则执行Step5; Step5:算法停止,输出结果。 |
FI策略下的λ互换下降法运算流程如下:
Step1:通过特定方式生成初始可行解S,解中路径数量为K; Step2:路径对集合为RS,为路径对的数量; Step3:令i=1; Step4:通过λ互换机制互换RS中第i对路径,生成S的邻域解S'; Step5:计算新解S'的效应值; Step6:如果,令S=S',继续执行Step3;否则执行Step7; Step7:令i=i+1,如果i≤M,执行Step4;否则执行Step8; Step8:算法停止,输出结果。 |
5.3.6 java编程实现
根据该算法的流程可知,λ互换下降法中需要进行两条车辆路径中节点的互换操作,并进行操作后的效应评价,所以下面先分别实现两条路径之间的(0,1)互换、(1,1)互换、(0,2)互换、(1,2)互换和(2,2)互换的操作过程。这五种互换过程涵盖了λ互换下降法中λ为1或为2时会发生的全部互换可能性,其中(0,1)、(0,2)和(1,2)互换的操作过程可以分别表示(1,0)、(2,0)和(2,1)互换的操作过程,只需要将传入的两条路径先后位置替换一下即可。
这五种互换操作将使用java类中的方法来实现,方法名称和参数分别设定为:lamdaExchange01(R1,R2,,,C,D)、lamdaExchange11(R1,R2,,,C,D)、lamdaExchange02(R1,R2,,,C,D)、lamdaExchange12(R1,R2,,,C,D)和lamdaExchange22(R1,R2,,,C,D),传入的六个参数依次为两个互换的路径对象、客户节点的载运量、全部节点之间的成本矩阵、车辆最大载运量和车辆单次最大行驶里程。
令传入路径R1和R2的首尾均需为数字0,表示路径起点和终点均为车场,其他客户节点以数字码放在这两个0之间,例如{0,1,3,5,2,0}表示车辆从车场出发后依次经过客户1、3、5、2之后又返回车场;路径R1和R2中客户节点数量分别为和,全部节点数量分别为客户节点数量加2。
算法在运行过程中将根据R1和R2中节点数量来选择满足路径中节点数量的条件,即路径中节点数量不能少于其换出的节点数量,例如lamdaExchange11方法要求R1中客户节点数量不能少于1,R2中客户节点数量也不能少于1个;lamdaExchange02方法要求R2中客户节点不能少于2个,而R1中客户节点可以为0。
下面给出这些方法的具体实现过程。
- (0,1)互换编程
实现(0,1)互换过程是将R2中的客户点i转移到R1中去形成两条新路径和,然后对新路径进行近似评价,如果效应值为负,即两条新路径对应的总成本比两条旧路径对应总成本低,则再对新路径进行优化,并确定为最后的新路径。
在(0,1)互换中,R2中有个客户节点,R1中有个客户节点,运算过程需要先估计每个换出节点i的近似效应值,具体操作是保持R1和R2中各个客户节点先后顺序不变,将R2中的节点i提取出来依次插入到R1中节点排序之间,获得R1和R2的新解,然后判断新解和旧解对应目标函数的差值作为该操作的效应。以R1和R2中分别有三个节点为例,图1给出了每个换出节点可以产生的近似解。
(a) (b) (c) (d)
图1 (0,1)互换近似估计新解生成示意图
图1中(a)为原有解R1和R2,图 (b)、(c)和(d)分别为将R2中的节点4、5和6换入R1后形成的近似新解,其中圆头虚线表示换入节点在R1中的插入位置。运算过程计算这些近似新解的目标函数值,同(a)的目标函数值比较,记录每个近似解的效应值。
不论是BI策略还是FI策略,最后都是选择能够互换操作能够产生较好解的结果作为下一次迭代的解,所以方法lamdaExchange01通过输入的参数进行运算后,R2中每个客户节点插入R1中的多个可行解中只保留一个最好的互换结果,符合算法流程的逻辑。图1中(b)表示节点4换入R1中可能有四个可行解,只保留一个R1新路程总里程最小的解即可,同时图(c)和图(d)中节点5和6分别插入R1后也各自选择一个最好的解,然后再对 (b)、(c)和(d)所选的最好结果进行比较,返回三者之中的最好解作为互换后的结果。
根据上述设计可得出lamdaExchange01的伪代码如下:
输入参数 获取R1和R2中的客户节点数量和 Gaps:记录R2中每个节点换入R1之后的最好效应和新路径 For k=1 to i:R2中第k个客户 gapR2:=C(R2/i) – C(R2),R2路径中删除节点i后的效应 gapCosts: 记录i插入到R1不同位置的效应估计和新R1 for j=0 to gapCosts[j]= endfor optGap =minj{gapR2+gapCosts[j]},获得最好效应和新R1 Gaps[k]=optGap,记录每个换出客户i交换操作的最好效应和新R1、新R2 endfor |
- (1,1)互换编程
实现(1,1)互换过程是将R2中的客户点i和R1中的客户点j进行互换形成两条新路径和,然后对新路径进行近似评价,如果效应值为负,即两条新路径对应的总成本比两条旧路径对应总成本低,则再对新路径进行优化,并确定为最后的新路径。
在(1,1)互换中,两条路径R1和R2中的客户节点均和都不小于1,运算过程需要先估计每个交换节点对(i,j)的近似效应值。具体操作是保持R1和R2中各个客户节点先后顺序不变,将R2中的节点i提取出来依次插入到R1中去除节点j剩余节点排序之间,这样可以获得个新的R1,然后将这些新R1中效应值最低的选为最后的新R1;与之类似,将R1中的节点j提取出来依次插入到R2中去除节点i剩余节点排序之间,这样可以获得个新的R2,然后将这些新R2中效应值最低的选为最后的新R2。将最后获得的新R1和新R2作为两点交换的近似解。以R1和R2中分别有3个节点为例,图2给出了每个换出节点可以产生的近似解示意图。
(a) (b) (c) (d)
图2 (1,1)互换近似估计新解生成示意图
图2中(a)为原有解R1和R2,图 (b)、(c)和(d)为互换点对分别为(2,4)、(3,4)和(2,5)时采取(1,1)操作后可能形成的近似新解。运算过程计算这些近似新解的目标函数值,同(a)的目标函数值比较,记录每个近似解的效应值,然后给每个互换点对保留最好的新R1和新R2。
由图2可知,对于客户节点数分别为和的路径R1和R2进行(1,1)互换,计算复杂度为,最坏情况为
伪代码:
两路径R1和R2,以及相关参数 pairs:根据和形成的互换队列全排列 rows: pairs的行数,即上述全排列的数量 resultSet: 两点互换获得的结果集合,包括新的R1和R2及其对应的效应值 for i=1:rows nowPair: pairs[i] out1:从R1中找出来互换的客户节点,R1[nowPair[1]] preOut1、rearOut1:R1中处于out1之前和之后的节点索引号 out2:从R2中找出来互换的客户节点,R2[nowPair[2]] preOut2、rearOut2:R2中处于out2之前和之后的节点索引号 R10:将R1中剔除out1后剩下的路径 R20:将R2中剔除out2后剩下的路径 outCost1:=dist[preOut1][rearOut1] –dist[preOut1][out1]-dist[out1][rearOut1] outCost2:=dist[preOut2][rearOut2] –dist[preOut2][out2]-dist[out2][rearOut2]
cost1In2:将out1插入R20各个位置获得的效应值 for j=1: cost1In2[j]= dist[R20[j-1]][out1]+dist[out1][R20[j]]-dist[R20[j-1]][R20[j]] end optCost1In2:=cost1In2中的最小值 newR2:optCost1In2对应的新路径R2; cost2In1:将out2插入R10各个位置获得的效应值 for j=1: cost2In1[j]= dist[R10[j-1]][out2]+dist[out2][R10[j]]-dist[R10[j-1]][R10[j]] end optCost2In1:=cost2In1中的最小值 newR1:optCost2In1对应的新路径R1; changeCost: 两点互换的效应=outCost1+outCost2+optCost1In2+optCost2In1 resultSet+={newR1,newR2,changeCost} endfor optRoute1,optRoute2:从resultSet中找出最好的作为这两个新路径 |
- (0,2)互换编程
(0,2)互换过程是将第二条路经中的两个点放入到第一条路径中去,查看是否会产生更好的解。该过程中,路径R2中客户节点数量必然不小于2,而路径R1中客户节点数量可以为零,也可以存在客户节点,这使得互换的位置组合变得比较复杂。
(a) (b) (c) (d)
图3 (0,2)互换近似估计新解生成示意图
图3展示R2中三个节点每次换入两个节点到R1的可能情况,以(b)图中节点4和5换入R1为例,可以看出节点4有四个可能插入的位置,节点5也有四个可能插入的位置,而且当节点4和5同时插入R1原有路径中某个路段中时,节点4和节点5的先后关系可以形成两种情况,因此可以看出总的组合数量随着R1中节点数量的增加而增加,计算复杂度在。
- (1,2)互换编程
(1,2)互换过程是将第二条路经中的两个点放入到第一条路径中去,同时将第一条路径中的一个点放到第二条路径中去,然后查看是否会产生更好的解。该过程中,路径R2中客户节点数量必然不小于2,而路径R1中客户节点数量必然不小于1,这互换操作的位置组合更加复杂。
- (2,2)互换编程
(2,2)互换过程是将第二条路经中的两个点和第一条路径中的两个点进行互换,然后查看是否会产生更好的解。该过程中,路径R2和R1中客户节点数量必然都不小于2。
5.5 算法实验结果对比
5.5.1 实验设计
本章所描述的四种算法编写在easyopt.jar包中的VRP类文件中,各算法的具体名称如表5.5所示。
表5.5 四种算法在VRP类中的方法名称
算法代号 | 算法名称 | 方法名称 |
CWS | 序贯节约里程 | optBySavingCWSeq |
CWP | 并行节约里程 | optBySavingCWParallel |
MCWS | 改进序贯节约里程 | optByModSavingSeq |
MCWP | 改进并行节约里程 | optByModSavingParallel |
Sweep | 扫描算法 | optBySweep |
Lamda | λ互换下降法 | optByLamdaExchange |
下面使用算例En22K4、En30K3、En51K5、En76K10和En101K8的数据为例,测试这四大类算法的寻优性能。算法寻优性能主要指标是算法获取的车辆路径方案总行驶里程,次要指标为算法运算时间,改进节约里程法中λ参数设定为0.3、0.6和0.9三个数值。
5.5.2 实验程序设计
算法的程序如下,注意使用easyopt0302及其之后的包,包下载链接:www.iescm.com/easyopt/download。
import easyopt.common.EasyArray; import easyopt.instances.VRPinstance; import easyopt.vrp.VRP; import java.util.ArrayList; /**测试四大类CVRP的启发式算法运算性能 * */ public class TestVRPHeuristics { public static void main(String[] args) { testHeuristic(); } /**启发式算法运算性能测试主程序 * */ public static void testHeuristic(){ double[][] results = new double[5][10]; double[][] runTimes = new double[5][10]; for(int i=0;i<5;i++){ VRPinstance instance = new VRPinstance(); if(i==0) instance.initEn22K4(); if(i==1) instance.initEn30K3(); if(i==2) instance.initEn51K5(); if(i==3) instance.initEn76K10(); if(i==4) instance.initEn101K8();
//根据算例数据,生成VRP类中优化方法所需形式的参数 int truckCap = instance.capacity; int[] dmd = instance.dmdQty; double[][] posXY = instance.posXY;
//使用序贯节约里程法进行运算,并记录运算结果和运算时间 double sumCost =0; long timeStart=System.currentTimeMillis(); //get the now time (ms) ArrayList<VRP.Route> rList = VRP.optBySavingCWSeq(truckCap, dmd, posXY); for(VRP.Route r:rList){ sumCost+=r.sumCost; } results[i][0] = sumCost; long timeEnd=System.currentTimeMillis(); //get the now time (ms) runTimes[i][0] = (timeEnd - timeStart);//the running time(ms) //使用并行节约里程法进行运算,并记录运算结果和运算时间 sumCost =0; timeStart = timeEnd; ArrayList<VRP.Route> rList2 = VRP.optBySavingCWParallel(truckCap, dmd, posXY); for(VRP.Route r:rList2){ sumCost+=r.sumCost; } results[i][1] = sumCost; timeEnd=System.currentTimeMillis(); //get the now time (ms) runTimes[i][1] = (timeEnd - timeStart);//the running time(ms) //使用sweep算法进行运算,并记录运算结果和运算时间 sumCost = 0; timeStart = timeEnd; ArrayList<VRP.Route> rList0 = VRP.optBySweep(truckCap, 0, dmd, posXY); for(VRP.Route r:rList0){ sumCost+=r.sumCost; } results[i][2] = sumCost; timeEnd=System.currentTimeMillis(); //get the now time (ms) runTimes[i][2] = (timeEnd - timeStart);//the running time(ms) //使用λ互换算法进行运算,并记录运算结果和运算时间 sumCost = 0; timeStart = timeEnd; ArrayList<VRP.Route> rListLamda = VRP.optByLamdaExchange(truckCap, 0, dmd, posXY,1); for(VRP.Route r:rListLamda){ sumCost+=r.sumCost; } results[i][3] = sumCost; timeEnd=System.currentTimeMillis(); //get the now time (ms) runTimes[i][3] = (timeEnd - timeStart);//the running time(ms) //使用不同λ【注意该参数同λ互换法中参数的不同】参数下的改进节约里程法进行运算,并记录运算结果和运算时间 for(int j=0;j<3;j++){ sumCost =0; double lamda=0.3+0.3*j; timeStart = timeEnd; rList = VRP.optByModSavingSeq(truckCap, dmd, posXY,lamda); for(VRP.Route r:rList){ sumCost+=r.sumCost; } results[i][4 + 2*j ] = sumCost; timeEnd=System.currentTimeMillis(); //get the now time (ms) runTimes[i][4 + 2*j] = (timeEnd - timeStart);//the running time(ms) sumCost =0; timeStart = timeEnd; rList2 = VRP.optByModSavingParallel(truckCap, dmd, posXY,lamda); for(VRP.Route r:rList2){ sumCost+=r.sumCost; } results[i][4 + 2*j + 1 ] = sumCost; timeEnd=System.currentTimeMillis(); //get the now time (ms) runTimes[i][4 + 2*j + 1] = (timeEnd - timeStart);//the running time(ms) } } System.out.println("----------sum distances-----------"); EasyArray.printArray(results,0); System.out.println("----------running times(s)-----------"); EasyArray.printArray(runTimes,0); } } |
执行该方法后获得的输出如下:
----------sum distances----------- 458.0 387.0 397.0 383.0 427.0 444.0 448.0 399.0 458.0 387.0 ; 817.0 586.0 515.0 562.0 900.0 575.0 783.0 594.0 812.0 590.0 ; 734.0 591.0 544.0 579.0 833.0 568.0 720.0 579.0 697.0 578.0 ; 1061.0 907.0 899.0 880.0 1276.0 953.0 1078.0 897.0 1030.0 903.0 ; 1197.0 999.0 854.0 996.0 1555.0 936.0 1313.0 940.0 1294.0 960.0 ; ----------running times(s)----------- 7.0 0.0 182.0 6.0 1.0 0.0 1.0 0.0 1.0 1.0 ; 2.0 1.0 115.0 15.0 2.0 1.0 2.0 0.0 2.0 0.0 ; 2.0 0.0 269.0 5.0 4.0 1.0 5.0 1.0 2.0 1.0 ; 10.0 2.0 761.0 23.0 8.0 2.0 5.0 2.0 5.0 1.0 ; 6.0 1.0 721.0 15.0 2.0 2.0 4.0 1.0 3.0 2.0 ; |
5.5.3 实验结果对比分析
根据上述实验输出的结果,整理成表格形式列于表5.6和表5.7。
表5.7 算法最优解总里程对比表
算例 | CWS | CWP | SWEEP | LAMDA | λ=0.3 | λ=0.6 | λ=0.9 | |||
MCWS | MCWP | MCWS | MCWP | MCWS | MCWP | |||||
En22K4 | 458 | 387 | 397 | 383 | 427 | 444 | 448 | 399 | 458 | 387 |
En30K3 | 817 | 586 | 515 | 562 | 900 | 575 | 783 | 594 | 812 | 590 |
En51K5 | 734 | 591 | 544 | 579 | 833 | 568 | 720 | 579 | 697 | 578 |
En76K10 | 1061 | 907 | 899 | 880 | 1276 | 953 | 1078 | 897 | 1030 | 903 |
En101K8 | 1197 | 999 | 854 | 996 | 1555 | 936 | 1313 | 940 | 1294 | 960 |
从表5.7运算结果的总里程来分析可以得出如下结论:
(1)最优解仅出现在Sweep和Lamda两种算法中,在五种算例数据中,Sweep算法获得了3次最优解,Lamda互换法获得2次最优解。
(2)两两算法对比时,CWP优于CWS;MCWP一般情况也优于MCWS,除了在λ为0.3时求解En22K4;Sweep优于CWS和MCWS;Sweep一般情况下也优于CWP和MCWP;Lamda算法优于CWP、CWS和MCWS,有时优于MCWP。
从算法寻优性能看,针对具体问题时,可以采用Sweep、Lamda和并行运算的改进节约里程法来进行求解,然后择优选用。
表5.8 算法求解运算时间对比表(ms)
算例 | CWS | CWP | SWEEP | LAMDA | λ=0.3 | λ=0.6 | λ=0.9 | |||
MCWS | MCWP | MCWS | MCWP | MCWS | MCWP | |||||
En22K4 | 7 | 0 | 182 | 6 | 1 | 0 | 1 | 0 | 1 | 1 |
En30K3 | 2 | 1 | 115 | 15 | 2 | 1 | 2 | 0 | 2 | 0 |
En51K5 | 2 | 0 | 269 | 5 | 4 | 1 | 5 | 1 | 2 | 1 |
En76K10 | 10 | 2 | 761 | 23 | 8 | 2 | 5 | 2 | 5 | 1 |
En101K8 | 6 | 1 | 721 | 15 | 2 | 2 | 4 | 1 | 3 | 2 |
从表5.8的运算时间可以看出Sweep算法的运算时间特别长,远比其他算法运算时间要长。因此在问题规模特别大时,需要根据车辆路径方案总里程和运算时间两者之间来权衡以选择合适的算法。
参考文献
- T.J. Gaskell. Bases for vehicle fleet scheduling. Operational Research Quarterly, 1967(18):281-295
- P. Yellow. A computational modification to the savings method of vehicle scheduling. Operational Research Quarterly, 1970(21):281-283.
- Ibrahim Hassan Osman. Metastrategy simulated annealing and tabu search algorithms for the vehicle routing problem. 1993(41):421-451