车辆路径安排的遗传算法研究
1.1问题描述
1.1.1车辆路径问题
车辆路径问题(Vehicle Routing Problem-VRP)是为一些车辆(确定或不确定数量)确定访问一些客户的路径,每一客户被而且只被访问一次,且每条路径上的客户需求量之和不超过车辆的能力。目标是使总成本(如距离、时间等)为最小。有时间窗车辆路径问题(V ehicle Routing Problem with Time Windows-VRPTW)是在VRP上加上了客户的被访问的时间窗约束.在VRPTW问题中,除了行驶成本之外,成本函数还要包括由于早到某个客户而引起的等待时间和客户需要的服务时间。
近年来有许多学者运用遗传算法来求解车辆路径问题。从他们的成果来看,遗传算法求解车辆路径问题,与传统方法相比至少有两个优点。①如果使用数学规划直接求解VRP问题,在问题规模较大时,问题的求解将耗费系统巨大的空间与很长的时间资源。汪寿阳等人的研究指出,当运用整数规划求解一个包含50个客户、10个仓库、15辆汽车的实际问题时,规划的分支将有54510个,约束将有上千亿个。显然用传统的优化方法求解这样的问题是不经济的。而采用遗传算法求解,它的运算规模用种群规模的大小来确定,运算时间也可用进化代数来控制,求解大规模的VRP问题时优点突出。②使用数学规划求解问题时,往往需要预先给出一个可行解,然后逐步迭代优化,但是VRP问题的初始可行解往往难以获得。但遗传算法应用自然界优胜劣汰的规律,可以从非可行解开始计算,在计算过程中逐渐解出可行解及最优解,并淘汰不可行解
本文中采用遗产算法,实现了从某一总仓库,去往八个分仓库,并返回总仓库的车辆运输调度。具体问题如下描述:
1.1.2问题描述
随机生成一个有8个分仓库的VRP问题,规定各仓库坐标在[0,100]×[0,100]间随机生成,指定车容量为8。各分仓库的需求量在[0,4]间随机生成,满载系数a=0.85,使单辆车能承担更多的运输任务。取种群规模pop_size=20,进化代数gen=50.得VRP问题各项数据如表1-1所示。
仓库名称 | 仓库位置 | 仓库需求量 |
总仓库 | (91,97) | 0 |
仓库1 | (97,1) | 2.32 |
仓库2 | (79,52) | 3.14 |
仓库3 | (67,95) | 1.3 |
仓库4 | (16,59) | 1.94 |
仓库5 | (23,85) | 3.93 |
仓库6 | (17,96) | 1.68 |
仓库7 | (69,44) | 2.86 |
仓库8 | (69,19) | 1.4 |
交又概率为0.6。各仓库间运输成本c,由各仓库间的直线距离决定,即:
根据各仓库的需求量,计算出需要的货车数。
1.1.3线性模型
令表示点i到点j的运输成本,本文中用路程描述。总仓库的编号为0,各分仓库的编号为i(i=1,2,…,8),定义变量如下:
可以得到该问题的车辆调度数学模型:
2.遗传算法
遗传算法基本流程框架如下图所示:
2.1遗传编码
车辆路径问题的染色体使用自然数编码,其解向量可编成一条长度为8+3+1的染色体:
[0. 2. 7. 0. 8. 3. 0. 5. 6. 4. 1. 0.]
在整条染色体中,自然数,表示第k个分仓库,0的数目为4个,代表总仓库,并把自然数编码分为3段,形成3个子路径,表示由3辆车完成所有运输任务。这样的染色体编码可以解释为:第1辆车从总仓库出发,经过2,7分仓库后,回到总仓库,形成了路径1;第2辆车也从总仓库出发,途经8,3分仓库后,回到总仓库,形成了路径2;第3辆车也从总仓库出发,途经5,6,4,1分仓库后,回到总仓库,形成了路径3,完成所有运输任务,构成了3条路径。
子路径1:总仓库→分仓库2→分仓库7→总仓库;
子路径2:总仓库→分仓库8→分仓库3→总仓库;
子路径3:总仓库→分仓库5→分仓库6→分仓库4→分仓库1→总仓库。
在编码过程中遵守特定的编码规则:
- 生成一个自然数(仓库个数)的全排列
- 将m(车辆数)+1个0插入到全排列
- 首尾必须是0,且两个0不能相邻
2.2 适值计算
车辆路径问题的适值计算可以将容量约束式转为运输成本的一部分,运输 成本变为:
式中,M为一很大的正数,表示当一辆车的货运量超过其最大承载量时的惩罚系数。根据磁体的实际应用,本文另M=300。由于在计算适值时,我们一般需要找最大的,因此在输出运输成成本时,我们输出成本的倒数。
进一步运用下式可将运输成本转换成适值函数:
式中,;为第i条染色体的适值,;为当前种群中最优染色体的运输成本,;为第i条染色体的运输成本。
2.3 选择
本步采用轮盘赌进行选择。首先选择适应值最大的个体进入子代。然后由上一步计算得到适应值,根据适应值计算各个个体的累计函数,根据累计函数,进行轮盘赌选择19个个体。轮盘赌实现伪代码如下:
轮盘赌又称比例选择方法.其基本思想是:各个个体被选中的概率与其适应度大小成正比.
(1)计算出群体中每个个体的适应度f(i=1,2,…,M),M为群体大小;
(2)计算出每个个体被遗传到下一代群体中的概率:
(3)计算出每个个体的累积概率;
q[i]称为染色体x[i] (i=1, 2, …, n)的积累概率
(4)在[0,1]区间内产生一个均匀分布的伪随机数r;
(5)若r<q[1],则选择个体1,否则,选择个体k,使得:q[k-1]<r≤q[k] 成立;
(6)重复(4)、(5)共19次
2.4遗传操作
2.4.1 交叉
遗传操作调用了python中封装的geatpy库,其中交叉运算采用部分映射交叉PMX,调用的方法名为xovpm。
部分映射交叉PMX实现过程:
第一步,随机选择一对染色体(父代)中几个基因的起止位置(两染色体被选位置相同):
第二步,交换这两组基因的位置:
第三步,做冲突检测,根据交换的两组基因建立一个映射关系,如图所示,以2-0-5这一映射关系为例,可以看到第二步结果中子代1存在两个基因2,这时将其通过映射关系转变为基因5,以此类推至没有冲突为止。最后所有冲突的基因都会经过映射,保证形成的新一对子代基因无冲突:
2.4.2 变异
采用互换变异(reciprocal exchange mutation)。互换变异是随机选择两个不同位置上的基因,并将这两个位置的基因相互交换,如图
但是由于经过交叉以及变异后,染色体不再遵守路径优化问题的特殊的染色体编码形式,还要保证编码符合要求,具体要求见遗产编码的编码规则。所以需要做进一步的冲突检测,使染色体符合本文的要求。定义染色体修正汉顺sort_pop(),是新生成的子代染色体符合编码规则。
3.问题改进与实施
本节通过对相关文献的阅读,对该文中的路径优化问题进行了深入的探讨,对在原先的代码的基础上,提出了改进,但并未彻底实现。第四节代码源于第三节的探讨。
3.1带时间窗的路径规划
在上述车辆路径规划中我们并未讨论每个仓库的是否要求在哪一个时间段内将物资运输出去,但是在实际的生产运输之中,例如某像苏宁等电器商场在顾客购买商品,进行电器配送时,顾客可能会产生时间需求,既在某一时间段[a,b]内将货物运到,因此需要在VRP上进一步改进。
3.1.1改进所需的参数:
时间窗口,说明车辆必须在之前到达客户i;在前虽然车辆已经到达但仍要的等到
$ t_{i j}$ 每条弧(i,j)对应一个时间值
3.1.2 线性规划模型增加新约束:
3.1.3 遗传算法实现的改进
1.确定优先关系
优先关系指的是客户被服务的先后次序。它可以根据起点到各客户的距离确定,也可以根据每个客户的时间窗来确定,还可以通过加权因子由两者共同来确定。在满足车容量和时间窗的约束前提下,我们有理由访问与起点0距离成本较小的客户。
其中$\omega_{1}, \quad \omega_{2} \ $ 和 $ \omega_{3}$是权重系数
2.遗传算法改进
3.2不确定车辆数
在前边的计算中,一开始我们将车辆数目根据相关经验公式计算得到m,在不确定问题中我们将车辆数目设为k,k并未赋值,在计算结束后,我们同时要生成车辆的数目。
模型中,式(3)、(4)代表目标函数,分别是最短路径长度、最少车辆数;式(5)表示派出车辆的数目不能超过中心仓库所拥有的车辆数;式(6)确保车辆都是从仓库出发,并回到仓库;式(7)、(8)保证每个客户只能被一辆车服务一次;式(9)定义了车辆容量约束;式(10)是时间窗约束。
4.计算结果
初始种群 [[0. 2. 7. 0. 8. 3. 0. 5. 6. 4. 1. 0.]
[0. 1. 7. 0. 8. 0. 6. 3. 5. 2. 4. 0.]
[0. 7. 3. 6. 0. 8. 0. 1. 4. 5. 2. 0.]
[0. 4. 2. 3. 0. 5. 8. 0. 1. 6. 7. 0.]
[0. 2. 0. 3. 1. 6. 5. 4. 0. 7. 8. 0.]
[0. 4. 5. 3. 0. 1. 2. 0. 8. 6. 7. 0.]
[0. 2. 5. 3. 0. 6. 8. 0. 7. 1. 4. 0.]
[0. 1. 6. 0. 3. 8. 2. 0. 7. 5. 4. 0.]
[0. 1. 7. 0. 6. 0. 5. 3. 4. 8. 2. 0.]
[0. 4. 0. 8. 1. 5. 0. 7. 2. 6. 3. 0.]
[0. 1. 2. 3. 0. 8. 6. 0. 7. 5. 4. 0.]
[0. 6. 4. 2. 0. 1. 0. 7. 3. 8. 5. 0.]
[0. 7. 8. 0. 5. 0. 3. 1. 6. 2. 4. 0.]
[0. 2. 3. 6. 0. 7. 4. 1. 0. 5. 8. 0.]
[0. 6. 2. 0. 1. 3. 0. 8. 7. 4. 5. 0.]
[0. 2. 4. 8. 0. 6. 3. 5. 0. 1. 7. 0.]
[0. 3. 8. 2. 1. 0. 7. 4. 0. 5. 6. 0.]
[0. 7. 5. 1. 0. 6. 8. 0. 4. 3. 2. 0.]
[0. 5. 0. 4. 3. 0. 2. 1. 7. 8. 6. 0.]
[0. 5. 0. 2. 7. 3. 1. 8. 0. 6. 4. 0.]]
最好的染色体出现在第43代
最好的染色体适应值为: 330.8350525686248
最好的染色体: [0. 4. 6. 2. 8. 7. 0. 5. 1. 0. 3. 0.]
运算结果:
子路径1:0—> 4—>6—>2—>8—>7---->0
子路径2:0—>5—>1—>0
子路径3:0—>3—>0
运输成本:330.84
5.代码
python3.7,pycharm。此代码截取部分!!!!!!!!!!!!!!!!!!!!!!!
import random
import itertools
# import math
import sys
import geatpy
import numpy as np
# gen=50 #进化代数
# n = 9 # 总的仓库数
# m = 3 # 所调用的车的数量
# c = 8 # 车的容量
# pop_size = 20 # 种群大小
# M = 300 # 重惩罚权值
# old_pop_list = [] # 初始父代种群
# distanceMatrix = [] # 两点之间距离
# location=() #初始化仓库位置
# demand = [] #初始化各仓库所需的运货量
# new_pop_list = np.zeros((pop_size, 12)) # 新的种群
# evaluation_list = [] # 每一代中所有染色体的适应度值的列表
# sum_evalution = 0 # 每一代中最大的适应度值
# best_fitness = 0 # 所有代中最好的适应度
# px = 0.6 # 交叉概率
# pm = 0.02 # 变异概率
#生成城市位置的随机坐标
def random_location():
location=[]
n = 9 #8个分仓库与一个总仓库
random_list = list(itertools.product(range(0, 100), range(0, 100))) #product(list1, list2) 依次取出list1中的每1个元素,与list2中的每1个元素,组成元组,
location=random.sample(random_list,n)
return location
# print(location)
#生成随机需求量(分仓库的需求量)
def random_demand():
demand = []
for i in range(8):
random_demand = np.random.uniform(0, 4)
demand.append(round(random_demand, 2))
demand.insert(0,0)
return demand
# demand=random_demand()
# print(demand)
#两仓库之间的计算距离
def distance( ):
location=random_location()
distanceMatrix=np.zeros(shape=(len(location),len(location)))
location=random_location()
# print(distanceMatrix)
for i in range(len(location)):
i -= 1
for j in range(len(location)):
j-=1
if i!=j:
x=location[i][0]-location[j][0]
y=location[i][1]-location[j][1]
distanceMatrix[i][j]=np.sqrt(x * x + y * y)
else:
continue
return distanceMatrix
# distanceMatrix=distance(location)
# print(distanceMatrix)
# 遗传编码(初始化种群)
# 编码生成规则
# 1.生成一个自然数(仓库个数)的全排列
# 2.将m(车辆数)+1个0插入到全排列
# 3.首尾必须是0,且两个0不能相邻
def decode( ):
#生成len(location)-1个初始种群
temp = []
location=random_location()
p_bool = True
p=1
pop_size=20 #种群大小
pop_list=np.empty(shape=[pop_size,12])
for k in np.arange(0,20):
pop = []
temp=random.sample(range(1,len(location)),len(location)-1) #生成随机数
for i in range(len(location)-1):
pop.append(temp[i])
pop.insert(0,0) #在第一位插入0
pop.insert(9,0) #在最后一位插入0
# return pop
#在位置2~9中间插入0,且0不能同时出现在相邻的位置
while p_bool:
p1=random.randint(2,len(location)-2)
p2=random.randint(2,len(location)-2)
p=abs(p1-p2) #判断p1与p2是否相邻
# print("p的值",p)
if p>1: #如果p值大于1则不相邻,符合要求
pop.insert(p1,0)
pop.insert(p2,0)
# print("pop的值",pop)
for j in range(12):
pop_list[k][j] = pop[j]
# print("pop_list的值",pop_list)
break
else:
continue
return pop_list
# print(old_pop_list)
old_pop_list=decode()
#计算适值评价
def caculateFitness(pop, c,M):
current_total = 0.0 # 当前的车的总容量
distance_total = 0.0 # 车的总运行距离
distance_total_list = [] # 每辆车行驶总距离存储列表
evaluation = 0.0 # 评价值
distanceMatrix = distance()
demand = random_demand()
# 根据初始化的种群计算车的行驶路程与车的容量
# 求在pop中0出现的位置列表temp
temp = []
for i in range(0, len(pop)):
j = pop[i]
if j == 0:
temp.append(i)
# 计算每辆车总行驶的距离,与离开每个仓库时,车上装运的货物的总容量
#如果到达某一个仓库时
for i in np.arange(0, len(temp)-1):
interm_list = np.arange(temp[i], temp[i + 1])
for k in interm_list:
# print("控制循环的k", k)
j = int(pop[k])
i = int(pop[k + 1]) #必须加int,否则报错only integers, slices (`:`), ellipsis (`...`)
if j == 0:
distance_total += distanceMatrix[i][0]
current_total += demand[j]
if current_total > c:
distance_total += M*abs(current_total-c)
# print("j=", j, distance_total,"目前总容量",current_total)
else:
distance_total += distanceMatrix[i][j]
current_total += demand[j]
if current_total > c:
distance_total +=M*abs(current_total-c)
# print("i", i, distance_total,"目前总容量",current_total)
evaluation += distance_total
distance_total = 0
current_total = 0
return 10/evaluation #evalution代表的时运输成本的评估值,应该是越小越好,因此此地返回一个evalution的倒数
#计算初始单个染色体的评价值,与总体的评价值,为染色体选择做准备
def sum_evalution(old_pop_list,c,M):
evaluation_list=[]
sum_evalution = 0
old_pop_list=old_pop_list
for i in range(20):
# print("=============================第%s个种种群===============================" %(i))
pop = old_pop_list[i]
evaluation= 0.0
evaluation = caculateFitness(pop,c,M) #单个的evalution
evaluation_list.append(evaluation)
# print("单个的evalution", evaluation)
sum_evalution += evaluation #整个种群的sum_evalution
# print("整个种群的sum_evalution", sum_evalution)
return (evaluation_list,sum_evalution)
# location = random_location() #仓库位置
# demand = random_demand() #仓库所需的运货量
# distanceMatrix = distance( ) #两点之间距离
# old_pop_list = decode( ) #遗传编码
# print(sum_evalution(old_pop_list))
# # print(sum_evalution( ))
# # evaluation_list = sum_evalution( )[0]
# # sum_evalution=sum_evalution( )[1]
# print("evaluation_list",evaluation_list,"sum_evalution",sum_evalution)
# # print("evaluation_list",evaluation_list)
#求适值函数,各个个体累计概率
def evalution_function(evaluation_list,sum_evalution):
f=[]
g=[]
for i in np.arange(0,20):
if i == 0:
g.append(evaluation_list[i] / sum_evalution)
# print("个体%s的累计概率"%i,g)
f.append(g[i])
else:
g.append((evaluation_list[i]/sum_evalution)+g[i-1])
f.append(g[i])
# print("个体%s的累计概率"%i,"f的值",f)
# print("===========================")
# g.append((evaluation_list[i] / sum_evalution)+evaluation_list[i-1])
# print("个体%s的累计概率"%i,"g的值",g)
return f
# function=evalution_function(evaluation_list,sum_evalution)
# print(function[0],function)
# print("各个个体累计概率",evalution_function(evaluation_list,sum_evalution))
#选择种群中适应度最高的,也就是函数caculateFitness返回值最大的,既def sum_evalution( )中返回的evaluation_list中最大的
#选择种群中适应度最高,最高适应度对应的下标
def maxevalution(evaluation_list):
max_fitness=0.0 #每一代种群中的最大适应度
max_id=0 #每个种群中最大的适应度所对应的下标
for i in np.arange(0,20):
if max_fitness > evaluation_list[i]:
continue
else:
if max_fitness < evaluation_list[i]:
max_fitness=evaluation_list[i]
max_id=i
return (max_fitness,max_id)
# max_fitness = maxevalution(evaluation_list)[0]
# max_id = maxevalution(evaluation_list)[1]
# print(maxevalution(evaluation_list))
#染色体复制,从old_pop_list中将挑选的染色体复制到新的new_pop_list中
def copypop(new_pop_list, old_pop_list,new_id,old_id):
old_id=old_id #父代中最大的适应度所对应的下标
new_l=new_id #新的种群的位置
for i in np.arange(0,12):
new_pop_list[new_l][i]=old_pop_list[old_id][i]
return new_pop_list
#在[0,1]之间的生成一个随机数
def randomFinger(old_pop_list,c,M):
x = 0.0
a=0
evalution_list = sum_evalution(old_pop_list, c, M)[0]
# print(evalution_list)
sum_e = sum_evalution(old_pop_list, c, M)[1]
function = evalution_function(evalution_list, sum_e)
a=function[11]
print(a)
for i in range(1):
x = np.random.uniform(0,a)
return x
# x=0.0
# x=randomFinger(old_pop_list,8,3)
# print(x)
#进化函数,保留最优
def evalutionFunction(new_pop_list,old_pop_list, evaluation_list,function):
#找出最大适应度的染色体,把他放到新的种群第一个,既子代的第一个。
c=8
M=3
max_id = maxevalution(evaluation_list)[1]
new_pop_list = copypop(new_pop_list, old_pop_list, 0, max_id)
# print("父代中适应度最高的进入子代形成的new_pop_list",new_pop_list)
#赌轮选择策略挑选scale-1个下一代个体
k=0
for i in np.arange(1,20):
x = randomFinger(old_pop_list,c,M)
# print("产生的随机数",x)
# print("控制循环的I", i)
for j in np.arange(20):
# print("function",function[j])
if j == 0:
if x < function[0]:
new_pop_list= copypop(new_pop_list, old_pop_list, i,0)
# print("判断轮盘赌第一个是否能进入",new_pop_list)
# break
else:
if j !=0:
if x < function[j] and x > function[j-1]:
new_pop_list = copypop(new_pop_list, old_pop_list,i, j)
# print("判断进入的先后顺序%d"%j,new_pop_list)
# break
return new_pop_list
# new_pop_list= evalutionFunction(new_pop_list ,evaluation_list,function)
# print(new_pop_list)
#交叉和变异
# new_pop_list=geatpy.xovpm(new_pop_list,px)
# print(new_pop_list)
# FieldDR=np.array([[0,0,0,0,0,0,0,0,0,0,0,0],
# [8,8,8,8,8,8,8,8,8,8,8,8]])
# new_pop_list=geatpy.mutbga(new_pop_list,FieldDR, pm).astype(np.int)
# print(new_pop_list)
#整理染色体,使其满足要求
def sort_pop(new_pop_list):
print("传入的new_pop_list", new_pop_list)
for i in np.arange(20):
pop=new_pop_list[i]
print("传入的pop",pop)
# 求在pop中0出现的位置列表temp
temp = []
for k in np.arange(0, len(pop)):
j = pop[k]
if j == 0:
temp.append(k)
# print("temp的值",temp)
#经过变异后可能头尾不再是0,变成0
if pop[0] !=0 or pop[11]!=0:
pop[0] = 0
pop[11] = 0
#再重新计算0的个数
temp=[]
for k in np.arange(0, len(pop)):
j = pop[k]
if j == 0:
temp.append(k)
#当0的个数大于4 时,说明缺失了某一个1~8的数,补上
d=len(temp)
k=1
if d > 4:
for i in np.arange(1,9):
if i not in pop:
p = temp[k]
# print("%d在数组内"%i)
pop[p] = i
k+=1
# else:
# print("%d不在数组内" % i)