遗传算法实践(九) VRP问题

VRP(Vehicle Routing Problem)问题描述

在VRP问题中,假设有一个供求关系系统,车辆从仓库取货,配送到若干个顾客处。车辆受到载重量的约束,需要组织适当的行车路线,在顾客的需求得到满足的基础上,使代价函数最小。代价函数根据问题不同而不同,常见的有车辆总运行时间最小,车辆总运行路径最短等。

这个问题基于以下假设:

  • 所有距离采用欧几里得表示
  • 每个顾客只接受一辆车的一次配送
  • 从配送中心出发的车辆必须返回配送中心
  • 每辆车载重量相同
  • 一条路径上顾客的总需求量不能超过车辆的最大载重量

定义为需要服务的两个顾客编号,为配送中心的车辆编号,为顾客和仓库的集合。
参数:

: 从顾客到顾客的行驶距离

:顾客的需求量

:车辆的最大载重量

决策变量:

:当车辆被分配从顾客运行到顾客时,取1;否则取0

在给定了参数和定义了决策变量之后,VRP问题可以用数学模型表示为:
min\sum_{e\in E}\sum_{j\in C}\sum_{k \in C}c_{jk}z_{ejk} \\s.t.\begin{aligned} \sum_{e\in E}\sum_{j\in C}z_{ejk} = 1, \forall j,k \in C \backslash \{0\} \\ \sum_{j\in C\backslash \{0\}}\sum_{k\in C\backslash \{0\}}d_k z_{ejk} \le Y_e, \forall i,e \in E \\ \sum_{k\in C}z_{e0k}=1, \forall e \in E \\ \sum_{j\in C\backslash \{0\}}z_{ejk} - \sum_{k\in C\backslash \{0\}}z_{ejk}=0, \forall e \in E \\ \sum_{j\in C_i}z_{ejk}=1, \forall e\in E \\ z_{ejk} = 0 or 1, \forall e \in E, \forall j,k \in C \end{aligned}

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,还原各条子路径

交叉操作

参考了大连海事大学硕士学位论文《基于电动汽车的带时间窗的路径优化问题研究》中的交叉操作,生成新的个体,具体描述如下图:

VRP问题交叉方法描述

突变操作

用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]

迭代过程如下图所示:

VRP_iterations

总共使用了4辆车,各自的行驶路径如下:

VRP_routes

你可能感兴趣的:(遗传算法实践(九) VRP问题)