论文复现丨带时间窗和服务顺序的多车辆路径问题:联合优化遗传算法


路径优化系列文章:

问题描述

某区域内有1个配送中心(配送中心集合记为J = {0}),分别用m种车型为n个顾客(顾客集合记为 I = {1, 2*, . . . , n})提供m种不同类型的服务(服务需求类型集合记为K* = {1, 2*,* · · · , m}),每种类型的车辆只能提供单一类型的服务,假设:

(1)各种类型的车辆均从配送中心出发,完成任务后仍回到原配送中心;

(2)每个顾客的一种需求只需要被同类型车辆服务一次;

(3)每条路径上的顾客总需求量不能超过车辆的最大服务能力;

(4)每辆车的总行驶时间不能超过规定的最长行驶时间;

(5)每个顾客的第1种需求对应的时间窗为软时间窗,服务车辆早到或晚到会产生等待或惩罚费用;

(6)具有多种服务需求的顾客,其各类需求的服务顺序为1*,* 2*,* · · · , m,即同一顾客的第k类服务完成时间必须早于第k + 1类服务的开始时间,且时间间隔不能超过最大允许时间间隔SLk(服务水平。

简单来说:客户点有多个服务需求,两个服务时间点差距必须在一定范围内,其余同一般车辆路径问题一样。文献的客户点有配送需求,安装需求(部分点)。

数学模型

文献的模型:

第一部分是车辆固定成本,第二部是行驶成本,第三部分是时间窗成本。

数据:

包含参考文献参考数据和rc101到rc108成本:

文献参考数据

rc数据:

其中论文参考数据没有时间窗(可以自行设计),解码时不计算,rc数据无安装点,按照文献任意生成40个。

代码运行环境
windows系统,python3.6.0,第三方库及版本号如下:

numpy==1.18.5
matplotlib==3.2.1

第三方库需要在安装完python之后,额外安装,以前文章有讲述过安装第三方库的解决办法,点击下方文章名字跳转:

编码解码

1、编码

随机打乱配送点和安装点即可得到配送路径编码和安装路径编码。

2、配送路径解码

依次遍历配送路径,载重约束下车辆尽可能服务更多点:

step1:初始只有一辆车,载重为0,遍历路径第一个点,更新载重,更新车辆路径;

step2:不断遍历路径的点,更新载重和车辆路径,遍历到某点不满足载重时转入step3;,否则遍历完所有点结束;

step3:新加入车辆,载重更新为该点的需求量,新车辆路径加入该点。

核心代码如下:

    while road:                                  # 当路径还有点未分配给车
        DE_idx.append(road[0])                   # 每次车辆路径加入路径第一个点
        if sum(np.array(self.DE)[DE_idx])<=self.load:  
            road.pop(0)                          # 如果满足载重约束,删除路径第一个点          
        else:
            road_car.append(DE_idx[:-1])         # 如果不满足,截取上一个车辆路径
            DE_idx = []                          # 新车辆路径从空开始
    road_car.append(DE_idx)                      # 循环完成添加最后一辆车辆路径

得到各车辆的路径后,计算成本比较容易,路径数就是车辆数,任意2点距离除速度得到时间,累积时间就可以判断是否违反时间窗:

def caculate(self,road_car,signal):
        t_arrive = []
        t_a = [0 for w in self.AZ]                  # 初始安装点的配送时间为0
        z = len(road_car)
        for rc in road_car:                         # 每辆车的路径
            t = 0
            beg_ = 0                                # 从中心0开始
            for r in rc + [0]:                      # 最后回到中心0
                dis = np.sqrt((self.XY[r][0] - self.XY[beg_][0])**2 + (self.XY[r][1] - self.XY[beg_][1])**2)
                t += dis/self.v                     # 计算到达时间
                if r in self.AZ:                    # 如果点属于安装路径
                    idx = self.AZ.index(r)          # 找到对应点在安装路径编码的位置
                    t_a[idx] = t                    # 更新安装点到达时间
                t_arrive.append(t)                  # 更新每个点到达时间(包括回到中心点)
                beg_ = r
        w = self.gk*z + self.ck*sum(t_arrive)       # 计算不包含时间窗的成本
        if self.WD and signal:                      # 如果时间窗存在且需要计算(安装路径时不需要)
            c = 0
            u = 0
            for rc in road_car:                     # 路径成本
                for r in rc + [0]:
                    win_left = self.WD[r][0]        # 左时间窗
                    win_right = self.WD[r][1]       # 右时间窗
                    if t_arrive[c] < win_left:      # 时间窗成本计算,下同
                        u += 2*(win_left-t_arrive[c])
                    if t_arrive[c] > win_right:
                        u += 2*(t_arrive[c] - win_right) 
                    c += 0
            w += u                                  # 成本更新
        return w,t_a

当有时间窗数据且需要计算才会计算时间窗成本(安装点路径不需要计算) ,t_a是为了下面的安装路径解码。

3、安装路径解码

依次遍历安装路径,安装路径的点必须和配送时间点在一定范围内(2种情况:早于安装时间;晚于配送时间时间时在允许范围内到达),否则该点加入新车:

step1:初始只有一辆车,载重为0,遍历安装路径第一个点,更新时间(安装车到达时间和配送车到达时间的最大值),更新车辆路径;

step2:不断遍历路径的点,更新时间和车辆路径,遍历到某点时间不满足间隔约束时转入step3;,否则遍历完所有点结束;

step3:新加入车辆,时间更新为中心到该点的时间,新车辆路径加入该点。

核心代码:

for i in range(len(road_car)):         # 遍历目前的安装车
    if i==0:                           # 初始从配送中心出发
        beg_ = 0
    else:
        beg_ = road_car[i][-1]         # 否则从某辆车的最后一个点出发
    dis = np.sqrt((self.XY[r][0] - self.XY[beg_][0])**2 + (self.XY[r][1] - self.XY[beg_][1])**2)
    t = dis/self.v + ta[i]             # 计算到达时间
    if t - t_arrive <= self.slk:       # 如果到达时间在可接受范围
        road_car[i].append(r)          # 车辆加入该点
        ta[i] = max(t, t_arrive)       # 更新该点的完工时间(安装到达时间和配送时间最大值)
        signal = 1
        break
if signal == 0:                        # 如果所有车都没加入点
    beg_ = 0
    dis = np.sqrt((self.XY[r][0] - self.XY[beg_][0])**2 + (self.XY[r][1] - self.XY[beg_][1])**2)
    road_car.append([])               # 添加新车
    t = dis/self.v
    ta.append(max(t, t_arrive))       # 更新该点的完工时间
    road_car[-1].append(r)            # 新车路径加入点

成本计算同上。

算法改进

1、算法步骤

轮盘赌选择,概念可以自行查阅,用np.random.choice实现:

def select(self,pop,pop1,fit):          # 轮盘赌选择
        fit=np.array(fit)
        idx=np.random.choice(np.arange(self.pop_size),size=self.pop_size,replace=True,
                             p=fit/fit.sum())
        return pop[idx],pop1[idx]

idx是轮盘赌选择得到的索引,因为文献存在配送路径和安装路径,返回2个种群。

次序交叉:

任意生成2点,固定2点之间的基因和位置,处理其余冲突基因再重新拼接。

如B处理冲突的1,4,3剩余[2,7,5,6,8],[1,4,3]左拼接[2,7],右拼接[5,6,8]得到子代A,子代B同理。

代码:

def crosss(self, a, b):                    # 次序交叉
    loc = random.sample(range(len(a)), 2)  # 生成2个位置
    loc.sort()                             # 升序
    id1, id2 = loc[0],loc[1]               # 取对应位置点
    change_a = a[id1:id2]                  # 截取交换位置          
    change_b = b[id1:id2]                  # 截取交换位置

    for ca in change_a:                    # 父代b移除a交换的对应基因
        b.remove(ca)
    a1 = b[:id1] + change_a + b[id1:]      # b剩余基因和交换基因得到子代a1

    for cb in change_b:                    # 父代a移除b交换的对应基因
        a.remove(cb)
    b1 = a[:id1] + change_b + a[id1:]      # a剩余基因和交换基因得到子代b1
    return a1, b1                         

变异是交换一个编码任意2个位置的基因:

def mul(self,a):
    loc = random.sample(range(len(a)), 2)  # 生成2个位置
    a[loc[0]], a[loc[1]] = a[loc[1]], a[loc[0]] # 对应基因交换
    return a

保留top N个体,比较容易,每次迭代合并父代和子代,保留最优秀的前N个个体:

核心代码:

T_r, T_rz =np.vstack((T_road,T_road1)),np.vstack((T_road_az,T_road_az1))  # 合并父代子代路径
ans = answer + answer1                             # 合并父代子代目标

index_sort=np.array(ans).argsort()                 # 目标最小到大的索引
T_road, T_road_az = T_r[index_sort][0:self.pop_size],  T_rz[index_sort][0:self.pop_size] # top N个体
answer = np.array(ans)[index_sort][0:self.pop_size].tolist() # 对应目标

运行结果

主函数

设计主函数如下:

import data
import numpy as np
from decode import de
import random
from ga_combine import ga_c

data_ = 'RC101'          # 可选择101到108,运行rc数据时记得屏蔽data_ = 0
data_ = 0                # 运行论文数据

if data_:
    XY, DE, WD = data.read_rc(data_)
    AZ = random.sample(range(1, len(DE)), 40) # 生成40个安装点
else:
    XY, DE, AZ = data.read()
    
gk = 20     # 单个车辆固定费用
ck = 1      # 单位距离运行成本
v = 1       # 车辆速度
if data_:        # rc系列数据时
    load = 1000  # 载重
    slk = 300    # 允许安装时间和配送时间间隔
else:
    load = 15    # 载重
    slk = 40     # 允许安装时间和配送时间间隔
    WD = []      # 论文数据没时间窗,可自行设计

generation, popsize, cr, mu = 20, 40, 1, 1          # 迭代次数,种群规模,交叉概率,变异概率
d = de(gk, ck, v, load, XY, DE, AZ,slk,WD)          # 解码方法
gc = ga_c(generation, popsize, cr, mu, d)           # 遗传方法
road, road_z, result = gc.total(len(DE)-1, len(AZ)) # 算法结果

road,road_car = d.creat(road) 
w1,t_a = d.caculate(road_car,1)
road_car_az = d.caculate_az(road_z,t_a)
w2,t_a = d.caculate(road_car_az ,[])

print('\n配送编码:',road)
print('车辆配送路径:',road_car)
print('\n安装编码',road_z)
print('车辆安装编码:',road_car_az)
print(f'成本验证:{w1+w2:.2f}')

d.draw(road_car, road_car_az, w1, w2,data_)  # 路径图
d.draw_change(result)                        # 迭代图

运行结果

论文数据20次结果:

路径图

迭代图

rc101数据20次结果

路径图

迭代图

小结

  • 本推文基本对参考论文进行复现,进行了论文数据和rc101到rc108的测试,希望有借鉴意义。excel数据和算法代码参数皆可修改(修改时注意不要改变excel数据的布局)。

  • 参考文献:带时间窗和服务顺序约束的多需求车辆路径问题 (李珍萍 张煜炜)

代码

为了方便和便于修改和测试,把代码在4个py文件里,pa.py是rc101-108的一个简单爬虫程序。

演示视频:

论文复现丨带时间窗和服务顺序的多车辆路径问题:联合优化遗传算法

完整算法+数据:

可加微信:diligent-1618,请备注:学校-专业-昵称。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值