赞
踩
VRP问题简介:
车辆路径问题(Vehicle Routing Problem):分配一组车辆去访问多个客户点,并在满足约束条件的情况下最小化总行驶距离或成本。详情见车辆路径问题(Vehicle Routing Problem,VRP) - 知乎
本文研究的是最基础的VRP问题:给定一组客户点、车辆容量、车辆数量、起始点和终点,目标是找到使得所有客户点都被访问一次的最短路径方案。
笔记是对遗传算法求解车辆路径优化问题VRP(Python代码实现)-CSDN博客的总结及更小白解释翻译。
下面上写代码步骤:
- geneNum = 100 # 种群数量
- generationNum = 300 # 迭代次数
-
- CENTER = 0 # 配送中心
- # HUGE = 9999999
- # PC = 1 #交叉率,没有定义交叉率,也就是说全部都要交叉,也就是1
- PM = 0.1 # 变异率 以前是用的vary
-
- n = 25 # 客户点数量
- m = 2 # 换电站数量
- k = 3 # 车辆数量
- Q = 5 # 额定载重量, t
- # dis = 160 # 续航里程, km
- length = n+m+1 # n个客户点+m个换电站+配送中心
-
- # 坐标 第0个是配送中心 1-25是顾客 26和27是换电站 一共28个位置 行驶距离要通过这个坐标自己来算
- X = [56, 66, 56, 88, 88, 24, 40, 32, 16, 88, 48, 32, 80, 48, 23, 48, 16, 8, 32, 24, 72, 72, 72, 88, 104, 104, 83,32]
- Y = [56, 78, 27, 72, 32, 48, 48, 80, 69, 96, 96, 104, 56, 40, 16, 8, 32, 48, 64, 96, 104, 32, 16, 8, 56, 32, 45, 40]
- # 需求量
- t = [0, 0.2, 0.3, 0.3, 0.3, 0.3, 0.5, 0.8, 0.4, 0.5, 0.7, 0.7, 0.6, 0.2, 0.2, 0.4, 0.1, 0.1, 0.2, 0.5, 0.2, 0.7,0.2,0.7, 0.1, 0.5, 0.4, 0.4]

编码:定义基因编码函数
产生初始个体----无序列表(首尾位置都有配送中心0,约束:一辆车运输的需求量总和不超过车的负载,插入0作为从配送中心新的开始,表示有几辆车),步骤如下:
- def getGene(length):
- #构建无序的列表
- data = list(range(1,length)) ##先产生一个有序的列表
- np.random.shuffle(data) ##有序列表打乱成无序列表
- data.insert(0, CENTER) ##在开始插入0
- data.append(CENTER) ##在结尾插入0
-
- #生成小车
- sum = 0 #初始小车容量为0
- newData = [] #定义插入小车后的新数据集
- for index, pos in enumerate(data): #获取旧data里的数据
- sum += t[pos] #按位置取出顾客点的需求装到车车上
- if sum > Q: #比较车量现在的装载量和满载容量
- newData.append(CENTER) #如果超过容量则生成新车车,即往新数据集里加0
- sum = t[pos] #新车在下一节点装货,更新新车车新容量
- newData.append(pos) #无论sum是否大于0,都把当前节点加入新数据集
-
- return newData #返回加入车车后的新数据集

用到的方法:
1.生成一个有序列表:list[range()]
2.打乱顺序:np.random.shuffle();shuffle在英语中就是洗牌,打乱顺序的意思
3.因为要累计车载量,所以用sum()。 enumerate()用来取pos,即顾客点的位置坐标,再代入t中取顾客点的需求。
4.for循环插0:它接受两个参数:data
和Q
。函数的目的是根据给定的条件对data
进行处理,并返回一个新的列表newData
,这里用来判断什么位置插0。
小白常识: enumerate()
是Python内置函数之一,用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标,一般用在for循环当中。
- fruits = ['apple', 'banana', 'orange']
- for index, fruit in enumerate(fruits):
- print(index, fruit)
-
- 输出形式如下:
- 0 apple
- 1 banana
- 2 orange
- def getpop(length,geneNum): #定义一个包含geneNum个种群个体的列表 ,每个的基因长度为length
- pop = [] #创建一个空列表pop,用于存储生成的基因序列
- for i in range(geneNum): #遍历每个个体
- gene = getGene(length) #给每个个体按之前定义的函数getGene编写基因
- pop.append(gene) #加入种群
- return pop #返回包含所有基因序列的列表pop
上述代码使用for
循环遍历geneNum
次。在每次循环中,调用getGene(length)
函数生成一个长度为length
的基因序列,并将其添加到pop
列表中。
- ##计算一个个体的适应度值(目标是距离最短)
-
- def getfit(gene):
- # 初始化
- distCost = 0 # 初始化距离成本distCost为0,
- dist = [] # 创建一个空列表dist用于存储每两个相邻点之间的距离
-
- # 计算距离
- i = 1 # while循环遍历每个元素
- while i < len(gene):
- calculateDist = lambda x1, y1, x2, y2: math.sqrt(((x1 - x2) ** 2) + ((y1 - y2) ** 2)) # 计算两点间距离
- dist.append(calculateDist(X[gene[i]], Y[gene[i]], X[gene[i - 1]], Y[gene[i - 1]])) # 添加到列表中
- i += 1
-
- # 距离成本
- distCost = sum(dist) # 总行驶距离
- fit = 1/distCost # 目标是最短距离,所以fit取倒数
- return fit # 返回适应度值
-
- ##得到整个种群的适应度列表
- def getfitness(pop):
- fitness = []
- for gene in pop:
- fit = getfit(gene) # gene代表的是种群中的个体
- fitness.append(fit) # 遍历后添加到适应度列表中,方便取用
- return np.array(fitness) # 使用numpy中的array()把列表转成矩阵形式

小白常识:计算距离时使用了匿名函数lambda简单定义了一个小型距离函数(作用是是代码精简易读),函数使用了math库中的sqrt函数(开平方根)来计算两点之间的欧氏距离。
lambda:
lambda的基本语法是:lambda parameters(参数): expression(表达式)
。如果需要传入多个参数,可以用逗号分隔开,例如:lambda x, y: x + y
。如果只需要一个参数,可以忽略其它的参数,例如:lambda x: x**2
。此外,也可以用默认参数的方式为lambda函数提供默认值。
在实际使用中,根据应用场景的不同,可以将lambda函数的用法扩展为以下几种:
lambda x, y: x + y
等价于def add(x, y): return x + y
。sorted([1, 2, 3], key=lambda x: x)
会根据列表元素的大小进行排序。filter(lambda x: x % 2 == 0, [1, 2, 3, 4, 5])
会返回一个包含所有偶数的列表。list(map(lambda x: x * 2, [1, 2, 3, 4, 5]))
将返回一个新的列表,其中每个元素都是原列表元素的两倍。sorted: 组织好的。sorted() 函数可以对列表、元组等可迭代对象进行排序。
步骤:
- def select(pop,fitness):
- fitness = fitness / fitness.sum() # 归一化,算概率
- idx = np.array(list(range(geneNum)))
- pop_idx = np.random.choice(idx, size=geneNum, p=fitness) # 根据概率选择个体
- for i in range(geneNum):
- pop[i] = pop[pop_idx[i]] #遍历取出选中的个体
- return pop
( 抽取范围a(一个列表),选几个(size=),是否重复抽取(replace=true(默认)),p=一维数组(与a一一对应)
)
步骤:
这里涉及遗传算法的交叉算子,可以了解一下。
(为什么这么做?)可能因为方便填充后面的基因 :使用有序交叉 (OX1)方法,致力于尽可能保留亲本基因的相对顺序。遗传算法中常见遗传算子 - 知乎
- #随机选择一段路径,并把路径移到最左边
-
- def moveRandSubPathLeft(gene):
- import random
- path = random.randrange(k) # 选择路径索引,随机分成k段
- print('path:',path)
- try:
- index = gene.index(CENTER, path+1) #移动到所选索引
-
- # 移动第一个配送中心
- locToInsert = 0
- gene.insert(locToInsert, gene.pop(index))
- index += 1
- locToInsert += 1
-
- # 移动此配送中心后的数据
- print('index:',index)
- try:
- print('len(gene):',len(gene))
- while gene[index] != CENTER:
- gene.insert(locToInsert, gene.pop(index))
- index += 1
- print('执行完index+1,index=',index)
- locToInsert += 1
- return gene
-
- # assert(length+k == len(gene))
- except:
- print('出错啦,index:',index)
- return gene
- except:
- print('0 is not in list',gene)
- return gene

这段代码是在遗传算法中选择一条随机路径,并将该路径中的某个中心点(CENTER)向左移动。具体来说,代码执行以下操作:
(不理解就问AI)
- try:
- # 尝试执行的代码块
- except ExceptionType:
- # 当发生指定类型的异常时执行的代码块
- finally:
- # 无论是否发生异常,都会执行的代码块(可选)
举例:
- # 创建一个列表
- my_list = [1, 2, 3, 4, 5]
-
- # 获取元素3的索引位置
- index = my_list.index(3)
- print(index) # 输出:2
-
- # 获取元素6的索引位置(不存在)
- try:
- index = my_list.index(6)
- print(index)
- except ValueError:
- print("元素6不存在于列表中") # 输出:"元素6不存在于列表中"
random.randrange()
是Python中的一个函数,用于生成指定范围内的一个随机整数
index的用法:list.index(要查找的元素, start位置, end)
再次选择适应度高的前1/3
- def choose(pop):
- num = int(geneNum/6) * 2 # 选择偶数个,方便下一步交叉
- key = lambda gene: getfit(gene) # 获得个体的适应度值记作key
- pop.sort(reverse=True, key=key) # 把pop用sort()函数降序排序, 按可选参数key排序
- # return shuffled top 1/3
- return pop[0:num]
- ##交叉一对
- def crossPair(i,gene1, gene2, crossedGenes):
- gene1 = moveRandSubPathLeft(gene1) #这是已经处理好的个体
- gene2 = moveRandSubPathLeft(gene2)
- newGene1 = []
- newGene2 = []
-
- # copy first paths
- centers = 0 # 记录中心点的数量
- firstPos1 = 1 # 记录中心点的位置
-
- # 遍历gene1,将前两个中心点及其之间的元素添加到newGene1中,并更新centers和firstPos1的值。
- for pos in gene1:
- firstPos1 += 1 #firstPos1表示当前处理的中心点的位置
- centers += (pos == CENTER) #如果pos等于CENTER,则 centers的值增加 1;否则不变。
- newGene1.append(pos)
- if centers >= 2:
- break
- # 同理处理gene2中的数据
- centers = 0
- firstPos2 = 1
- for pos in gene2:
- firstPos2 += 1
- centers += (pos == CENTER)
- newGene2.append(pos)
- if centers >= 2:
- break
-
- # copy data not exits in father gene
- for pos in gene2:
- if pos not in newGene1:
- newGene1.append(pos)
- for pos in gene1:
- if pos not in newGene2:
- newGene2.append(pos)
-
- # add center at end
- newGene1.append(CENTER)
- newGene2.append(CENTER)
-
- # 计算适应度最高的
- key1 = lambda gene1: getfit(gene1)
- possible1 = []
- try:
- while gene1[firstPos1] != CENTER: # 当基因序列中当前位置的元素不等于中心点时,执行循环体内的代码--复制。
- newGene = newGene1.copy()
- newGene.insert(firstPos1, CENTER)
- possible1.append(newGene)
- firstPos1 += 1
- print('第{}位置:{}'.format(i,len(possible1)))
- if len(possible1) == 0:
- crossedGenes.append(newGene1)
- else:
- possible1.sort(reverse=True, key=key1)
- crossedGenes.append(possible1[0])
- except:
- print('交叉出错啦:firstPos1', firstPos1)
-
- key2 = lambda gene2: getfit(gene2)
- possible2 = []
- try:
- while gene2[firstPos2] != CENTER:
- newGene = newGene2.copy()
- newGene.insert(firstPos2, CENTER)
- possible2.append(newGene)
- firstPos2 += 1
- print('第{}:{}'.format(i,len(possible2))) #打印当前处理的位置和可能的新基因序列的数量
- if len(possible2) == 0: #如果possible2列表为空,说明没有找到合适的新基因序列。
- crossedGenes.append(newGene2) #将原始基因序列添加到crossedGenes列表中
- else:
- possible2.sort(reverse=True, key=key2)#根据适应度对possible2列表进行降序排序。
- crossedGenes.append(possible2[0])
- print('交叉完成第:', i)
- except:
- print('交叉出错啦:',i)
-
- # 交叉
- def cross(genes):
- crossedGenes = []
- for i in range(0, len(genes), 2):
- # print('gene[i]:',genes[i])
- # print('gene[i+1]:', genes[i])
- crossPair(i,genes[i], genes[i+1], crossedGenes)
- print('交叉完成')
- return crossedGenes
-
- # 合并
- def mergeGenes(genes, crossedGenes):
- # sort genes with respect to chooseProb
- key = lambda gene: getfit(gene)
- genes.sort(reverse=True, key=key) ##先把原来的种群100按照适应度降序排列,然后,将交叉得到的32个个体替换到种群的最后32个
- pos = geneNum - 1
- for gene in crossedGenes:
- genes[pos] = gene
- pos -= 1
- return genes

上述代码实现了以下功能:
crossPair
函数:这个函数用于交叉两个个体(基因)。首先,它将两个个体的子路径向左移动,然后从左到右复制第一个个体的前两个子路径和第二个个体的前两个子路径。接下来,它将第二个个体中不在第一个个体中的部分添加到第一个个体中,将第一个个体中不在第二个个体中的部分添加到第二个个体中。最后,它在两个新个体的末尾添加中心点,并计算适应度最高的个体。
cross
函数:这个函数用于交叉一组个体(基因)。它遍历输入的基因列表,每次处理一对相邻的基因,并调用 crossPair
函数进行交叉。然后将交叉得到的新个体添加到结果列表中。
mergeGenes
函数:这个函数用于合并原始种群和新生成的个体。它首先根据适应度对原始种群进行降序排序,然后将新生成的个体替换到种群的末尾。
- # 变异一个
- def varyOne(gene):
- varyNum = 10
- variedGenes = []
- for i in range(varyNum): # 先按照这种方法变异10个,选择适应度最高的那个作为变异完的子代
- p1, p2 = random.choices(list(range(1,len(gene)-2)), k=2)
- # 从基因序列中随机选择两个位置(不包括第一个和最后一个元素)
- newGene = gene.copy() # 创建基因序列的一个副本,以避免修改原始基因
- newGene[p1], newGene[p2] = newGene[p2], newGene[p1] # 交换
- variedGenes.append(newGene) # 添加到队列
- key = lambda gene: getfit(gene) # 定义适应度匿名函数
- variedGenes.sort(reverse=True, key=key) # 以适应度降序排序
- return variedGenes[0] # 返回适应度最高的变异后的子代
-
- # 遍历变异
- def vary(genes):
- for index, gene in enumerate(genes):
- # 精英主义,保留前三十,这个意思就是前三十个一定不变异,到后面的个体才按照变异概率来变异
- if index < 30:
- continue
- if np.random.rand() < PM: # 以PM的概率变异
- genes[index] = varyOne(gene) # 更新
- return genes # 返回变异后的基因
-

- import numpy as np
- import random
- from tqdm import * # 进度条
- import matplotlib.pyplot as plt
- from pylab import *
- mpl.rcParams['font.sans-serif'] = ['SimHei']
- mpl.rcParams['axes.unicode_minus'] = False
-
- best_fitness = []
- min_cost = []
- J = []
- pop = getpop(length, geneNum) # 初始种群
- # 迭代
- for j in tqdm(range(generationNum)):
- print('j=',j)
- chosen_pop = choose(pop) # 选择 选择适应度值最高的前三分之一,也就是32个种群,进行下一步的交叉
- crossed_pop = cross(chosen_pop) # 交叉
- pop = mergeGenes(pop, crossed_pop) # 复制交叉至子代种群
- pop = vary(pop) # under construction
- key = lambda gene: getfit(gene)
- pop.sort(reverse=True, key=key) # 以fit对种群排序
- cost = 1/getfit(pop[0])
- print(cost)
- min_cost.append(cost)
- J.append(j)
-
- print(J)
- print(min_cost)
- print('\r\n')
- print('data:', pop[0])
- print('fit:', 1/getfit(pop[0]))
- plt.plot(J,min_cost, color='r')
- plt.show()

这段代码实现了遗传算法的基本步骤,包括选择、交叉、变异和排序等操作。在每次迭代中,选择适应度值最高的前三分之一种群进行交叉操作,然后将新生成的子代种群与原始种群合并。接下来,对种群进行变异操作,以增加种群的多样性。最后,根据适应度值对种群进行排序,并计算最优解的代价。这个过程会重复执行generationNum次,直到达到指定的迭代次数。
进度条:使用tqdm(range(generationNum))
时,它会创建一个进度条,显示当前迭代的进度。这在处理大量数据或需要长时间运行的任务时非常有用,可以让用户了解程序的运行状态。在深度学习等需要长时间运行的任务中,使用tqdm可以将训练过程以进度条的形式展现出来,使界面更加美观。
// 初始化
1. 随机生成初始群体
2. 设置交叉率和变异率
3. 设置停止条件
// 适应度函数
function fitness(individual):
1. 将每个个体转换为路线
2. 计算每个车辆的行驶距离和时间,以及超过容量的惩罚值
3. 将距离和时间的总和作为个体的适应度值
4. 返回适应度值
// 选择
function selection(population):
1. 使用轮盘赌算法选择父代
2. 返回两个父代
// 交叉
function crossover(parent1, parent2):
1. 随机选择交叉点
2. 交换交叉点后面的基因
3. 返回子代
// 变异
function mutation(individual):
1. 随机选择两个位置进行变异
2. 交换这两个位置的基因
3. 返回变异后的个体
// 主循环
1. 计算每个个体的适应度值
2. 如果停止条件已满足,则退出循环
3. 否则:
4. 选择两个父代
5. 通过交叉生成两个子代
6. 对每个子代进行变异
7. 将父代和子代合并成新的群体
8. 重复步骤1
虽然很简单,和实际有差距,但是思路还是有参考意义。
写在文末:
这篇文章只是遗传算法整个体系的一部分,还有许多要学的,比如交叉的具体情景还需要深入研究一下 。看博客文章做笔记还是太慢了(小白太费劲哈哈),我觉得需要去看些启发式算法以及运筹优化问题的基础建模思路的书籍,准备先去看看,有效果再回来还愿。。。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。