VRP(Vehicle Routing Problem)问题描述
在VRP问题中,假设有一个供求关系系统,车辆从仓库取货,配送到若干个顾客处。车辆受到载重量的约束,需要组织适当的行车路线,在顾客的需求得到满足的基础上,使代价函数最小。代价函数根据问题不同而不同,常见的有车辆总运行时间最小,车辆总运行路径最短等。
这个问题基于以下假设:
- 所有距离采用欧几里得表示
- 每个顾客只接受一辆车的一次配送
- 从配送中心出发的车辆必须返回配送中心
- 每辆车载重量相同
- 一条路径上顾客的总需求量不能超过车辆的最大载重量
定义为需要服务的两个顾客编号,为配送中心的车辆编号,为顾客和仓库的集合。
参数:
: 从顾客到顾客的行驶距离
:顾客的需求量
:车辆的最大载重量
决策变量:
:当车辆被分配从顾客运行到顾客时,取1;否则取0
在给定了参数和定义了决策变量之后,VRP问题可以用数学模型表示为:
VRP问题示例
给定车辆负载为400,各个节点的坐标和需求如下(节点0为配送中心):
节点编号i | 节点坐标(x,y) | 节点需求q |
---|---|---|
0 | (15, 12) | 0 |
1 | (3, 13) | 50 |
2 | (3, 17) | 50 |
3 | (6, 18) | 60 |
4 | (8, 17) | 30 |
5 | (10, 14) | 90 |
6 | (14, 13) | 10 |
7 | (15, 11) | 20 |
8 | (15, 15) | 10 |
9 | (17, 11) | 30 |
10 | (17, 16) | 20 |
11 | (18, 19) | 30 |
12 | (19, 9) | 10 |
13 | (19, 21) | 10 |
14 | (21, 22) | 10 |
15 | (23, 9) | 40 |
16 | (23, 22) | 51 |
17 | (24, 11) | 20 |
18 | (27, 21) | 20 |
19 | (26, 6) | 20 |
20 | (26, 9) | 30 |
21 | (27, 2) | 30 |
22 | (27, 4) | 30 |
23 | (27, 17) | 10 |
24 | (28, 7) | 60 |
25 | (29, 14) | 30 |
26 | (29, 18) | 20 |
27 | (30, 1) | 30 |
28 | (30, 8) | 40 |
29 | (30, 15) | 20 |
30 | (30, 17) | 20 |
遗传算法求解VRP问题
个体编码
对于个体采用自然数编码,0代表配送中心,1--n代表顾客;不同车辆的配送路线之间用0分隔(即每辆车都从仓库出发);对于有n个顾客,k辆车的VRP问题来说,染色体长度为n+k+1。
例如配送中心有3辆车为8个客户服务,一条可能的染色体如下:
0, 7, 0, 1, 2, 3, 5, 0, 8, 4, 6, 0
这条染色体表示的三辆车的行驶路线为:
第一辆车:0-7-0
第二辆车:0-1-2-3-5-0
第三辆车:0-8-4-6-0
解码
利用分割符0,还原各条子路径
交叉操作
参考了大连海事大学硕士学位论文《基于电动汽车的带时间窗的路径优化问题研究》中的交叉操作,生成新的个体,具体描述如下图:
突变操作
用2-opt算法对各条子路径进行局部优化
代码示例
## 环境设定
import numpy as np
import matplotlib.pyplot as plt
from deap import base, tools, creator, algorithms
import random
params = {
'font.family': 'serif',
'figure.dpi': 300,
'savefig.dpi': 300,
'font.size': 12,
'legend.fontsize': 'small'
}
plt.rcParams.update(params)
from copy import deepcopy
#-----------------------------------
## 问题定义
creator.create('FitnessMin', base.Fitness, weights=(-1.0,)) # 最小化问题
# 给个体一个routes属性用来记录其表示的路线
creator.create('Individual', list, fitness=creator.FitnessMin)
#-----------------------------------
## 个体编码
# 用字典存储所有参数 -- 配送中心坐标、顾客坐标、顾客需求、到达时间窗口、服务时间、车型载重量
dataDict = {}
# 节点坐标,节点0是配送中心的坐标
dataDict['NodeCoor'] = [(15,12), (3,13), (3,17), (6,18), (8,17), (10,14),
(14,13), (15,11), (15,15), (17,11), (17,16),
(18,19), (19,9), (19,21), (21,22), (23,9),
(23,22), (24,11), (27,21), (26,6), (26,9),
(27,2), (27,4), (27,17), (28,7), (29,14),
(29,18), (30,1), (30,8), (30,15), (30,17)
]
# 将配送中心的需求设置为0
dataDict['Demand'] = [0,50,50,60,30,90,10,20,10,30,20,30,10,10,10,
40,51,20,20,20,30,30,30,10,60,30,20,30,40,20,20]
dataDict['MaxLoad'] = 400
dataDict['ServiceTime'] = 1
def genInd(dataDict = dataDict):
'''生成个体, 对我们的问题来说,困难之处在于车辆数目是不定的'''
nCustomer = len(dataDict['NodeCoor']) - 1 # 顾客数量
perm = np.random.permutation(nCustomer) + 1 # 生成顾客的随机排列,注意顾客编号为1--n
pointer = 0 # 迭代指针
lowPointer = 0 # 指针指向下界
permSlice = []
# 当指针不指向序列末尾时
while pointer < nCustomer -1:
vehicleLoad = 0
# 当不超载时,继续装载
while (vehicleLoad < dataDict['MaxLoad']) and (pointer < nCustomer -1):
vehicleLoad += dataDict['Demand'][perm[pointer]]
pointer += 1
if lowPointer+1 < pointer:
tempPointer = np.random.randint(lowPointer+1, pointer)
permSlice.append(perm[lowPointer:tempPointer].tolist())
lowPointer = tempPointer
pointer = tempPointer
else:
permSlice.append(perm[lowPointer::].tolist())
break
# 将路线片段合并为染色体
ind = [0]
for eachRoute in permSlice:
ind = ind + eachRoute + [0]
return ind
#-----------------------------------
## 评价函数
# 染色体解码
def decodeInd(ind):
'''从染色体解码回路线片段,每条路径都是以0为开头与结尾'''
indCopy = np.array(deepcopy(ind)) # 复制ind,防止直接对染色体进行改动
idxList = list(range(len(indCopy)))
zeroIdx = np.asarray(idxList)[indCopy == 0]
routes = []
for i,j in zip(zeroIdx[0::], zeroIdx[1::]):
routes.append(ind[i:j]+[0])
return routes
def calDist(pos1, pos2):
'''计算距离的辅助函数,根据给出的坐标pos1和pos2,返回两点之间的距离
输入: pos1, pos2 -- (x,y)元组
输出: 欧几里得距离'''
return np.sqrt((pos1[0] - pos2[0])*(pos1[0] - pos2[0]) + (pos1[1] - pos2[1])*(pos1[1] - pos2[1]))
#
def loadPenalty(routes):
'''辅助函数,因为在交叉和突变中可能会产生不符合负载约束的个体,需要对不合要求的个体进行惩罚'''
penalty = 0
# 计算每条路径的负载,取max(0, routeLoad - maxLoad)计入惩罚项
for eachRoute in routes:
routeLoad = np.sum([dataDict['Demand'][i] for i in eachRoute])
penalty += max(0, routeLoad - dataDict['MaxLoad'])
return penalty
def calRouteLen(routes,dataDict=dataDict):
'''辅助函数,返回给定路径的总长度'''
totalDistance = 0 # 记录各条路线的总长度
for eachRoute in routes:
# 从每条路径中抽取相邻两个节点,计算节点距离并进行累加
for i,j in zip(eachRoute[0::], eachRoute[1::]):
totalDistance += calDist(dataDict['NodeCoor'][i], dataDict['NodeCoor'][j])
return totalDistance
def evaluate(ind):
'''评价函数,返回解码后路径的总长度,'''
routes = decodeInd(ind) # 将个体解码为路线
totalDistance = calRouteLen(routes)
return (totalDistance + loadPenalty(routes)),
#-----------------------------------
## 交叉操作
def genChild(ind1, ind2, nTrail=5):
'''参考《基于电动汽车的带时间窗的路径优化问题研究》中给出的交叉操作,生成一个子代'''
# 在ind1中随机选择一段子路径subroute1,将其前置
routes1 = decodeInd(ind1) # 将ind1解码成路径
numSubroute1 = len(routes1) # 子路径数量
subroute1 = routes1[np.random.randint(0, numSubroute1)]
# 将subroute1中没有出现的顾客按照其在ind2中的顺序排列成一个序列
unvisited = set(ind1) - set(subroute1) # 在subroute1中没有出现访问的顾客
unvisitedPerm = [digit for digit in ind2 if digit in unvisited] # 按照在ind2中的顺序排列
# 多次重复随机打断,选取适应度最好的个体
bestRoute = None # 容器
bestFit = np.inf
for _ in range(nTrail):
# 将该序列随机打断为numSubroute1-1条子路径
breakPos = [0]+random.sample(range(1,len(unvisitedPerm)),numSubroute1-2) # 产生numSubroute1-2个断点
breakPos.sort()
breakSubroute = []
for i,j in zip(breakPos[0::], breakPos[1::]):
breakSubroute.append([0]+unvisitedPerm[i:j]+[0])
breakSubroute.append([0]+unvisitedPerm[j:]+[0])
# 更新适应度最佳的打断方式
# 将先前取出的subroute1添加入打断结果,得到完整的配送方案
breakSubroute.append(subroute1)
# 评价生成的子路径
routesFit = calRouteLen(breakSubroute) + loadPenalty(breakSubroute)
if routesFit < bestFit:
bestRoute = breakSubroute
bestFit = routesFit
# 将得到的适应度最佳路径bestRoute合并为一个染色体
child = []
for eachRoute in bestRoute:
child += eachRoute[:-1]
return child+[0]
def crossover(ind1, ind2):
'''交叉操作'''
ind1[:], ind2[:] = genChild(ind1, ind2), genChild(ind2, ind1)
return ind1, ind2
#-----------------------------------
## 突变操作
def opt(route,dataDict=dataDict, k=2):
# 用2-opt算法优化路径
# 输入:
# route -- sequence,记录路径
# 输出: 优化后的路径optimizedRoute及其路径长度
nCities = len(route) # 城市数
optimizedRoute = route # 最优路径
minDistance = calRouteLen([route]) # 最优路径长度
for i in range(1,nCities-2):
for j in range(i+k, nCities):
if j-i == 1:
continue
reversedRoute = route[:i]+route[i:j][::-1]+route[j:]# 翻转后的路径
reversedRouteDist = calRouteLen([reversedRoute])
# 如果翻转后路径更优,则更新最优解
if reversedRouteDist < minDistance:
minDistance = reversedRouteDist
optimizedRoute = reversedRoute
return optimizedRoute
def mutate(ind):
'''用2-opt算法对各条子路径进行局部优化'''
routes = decodeInd(ind)
optimizedAssembly = []
for eachRoute in routes:
optimizedRoute = opt(eachRoute)
optimizedAssembly.append(optimizedRoute)
# 将路径重新组装为染色体
child = []
for eachRoute in optimizedAssembly:
child += eachRoute[:-1]
ind[:] = child+[0]
return ind,
#-----------------------------------
## 注册遗传算法操作
toolbox = base.Toolbox()
toolbox.register('individual', tools.initIterate, creator.Individual, genInd)
toolbox.register('population', tools.initRepeat, list, toolbox.individual)
toolbox.register('evaluate', evaluate)
toolbox.register('select', tools.selTournament, tournsize=2)
toolbox.register('mate', crossover)
toolbox.register('mutate', mutate)
## 生成初始族群
toolbox.popSize = 100
pop = toolbox.population(toolbox.popSize)
## 记录迭代数据
stats=tools.Statistics(key=lambda ind: ind.fitness.values)
stats.register('min', np.min)
stats.register('avg', np.mean)
stats.register('std', np.std)
hallOfFame = tools.HallOfFame(maxsize=1)
## 遗传算法参数
toolbox.ngen = 400
toolbox.cxpb = 0.8
toolbox.mutpb = 0.1
## 遗传算法主程序
## 遗传算法主程序
pop,logbook=algorithms.eaMuPlusLambda(pop, toolbox, mu=toolbox.popSize,
lambda_=toolbox.popSize,cxpb=toolbox.cxpb, mutpb=toolbox.mutpb,
ngen=toolbox.ngen ,stats=stats, halloffame=hallOfFame, verbose=True)
输出计算结果:
from pprint import pprint
def calLoad(routes):
loads = []
for eachRoute in routes:
routeLoad = np.sum([dataDict['Demand'][i] for i in eachRoute])
loads.append(routeLoad)
return loads
bestInd = hallOfFame.items[0]
distributionPlan = decodeInd(bestInd)
bestFit = bestInd.fitness.values
print('最佳运输计划为:')
pprint(distributionPlan)
print('最短运输距离为:')
print(bestFit)
print('各辆车上负载为:')
print(calLoad(distributionPlan))
# 画出迭代图
minFit = logbook.select('min')
avgFit = logbook.select('avg')
plt.plot(minFit, 'b-', label='Minimum Fitness')
plt.plot(avgFit, 'r-', label='Average Fitness')
plt.xlabel('# Gen')
plt.ylabel('Fitness')
plt.legend(loc='best')
# 计算结果
#最佳运输计划为:
#[[0, 9, 12, 19, 22, 24, 25, 17, 0],
# [0, 6, 4, 3, 2, 1, 5, 0],
# [0, 7, 0],
# [0, 8, 10, 11, 13, 14, 16, 18, 23, 26, 30, 29, 28, 27, 21, 20, 15, 0]]
#最短运输距离为:
#(136.93713103610511,)
#各辆车上负载为:
#[200, 290, 20, 391]
迭代过程如下图所示:
总共使用了4辆车,各自的行驶路径如下: