在现代物流和配送系统中,如何高效地规划车辆路径以满足客户需求,同时最小化成本和时间,是一个备受关注的问题。传统的优化方法在面对复杂约束时往往捉襟见肘,而遗传算法(Genetic Algorithms, GA)作为一种强大的进化计算技术,为解决此类问题提供了新的思路。本文将带您深入了解如何使用 Python 实现遗传算法来优化配送路径。
背景介绍
设想您是一家配送公司的运营经理,负责规划多辆配送车辆的路径,以确保所有客户订单能够准时送达,同时最小化油费、时间成本和过载成本。这个问题涉及多个变量和约束,如客户分布、车辆载重、续航里程以及时间窗口等,传统的优化方法难以在合理时间内找到最佳解。这时,遗传算法便成为一种理想的解决方案。
什么是遗传算法?
遗传算法是一种模拟自然选择和遗传机制的优化方法。它通过种群的进化过程,不断生成新一代的解,并通过选择、交叉和变异等操作,逐步逼近问题的最优解。其核心思想包括:
- 种群初始化:随机生成一组初始解(称为个体或基因)。
- 适应度评估:评估每个个体的优劣,适应度越高,表示该解越优。
- 选择:根据适应度选择优秀个体进行繁殖。
- 交叉:通过交换父代基因生成新的子代。
- 变异:随机改变子代基因,增加种群的多样性。
- 迭代:重复上述过程,直到满足停止条件(如达到最大迭代次数或找到满意解)。
问题描述
在本案例中,我们需要解决以下配送路径优化问题:
- 配送中心:所有配送路径的起点和终点。
- 客户点:25个分布在不同位置的客户,每个客户有不同的需求量和时间窗口。
- 换电站:2个用于车辆充电的站点。
- 车辆:3辆配送车辆,每辆车有最大载重和续航里程的限制。
- 目标:在满足所有客户需求和时间窗口的前提下,最小化油费、时间成本和过载成本。
实现步骤
1. 环境准备
首先,确保您已经安装了必要的 Python 库:
pip install matplotlib tqdm
import random
import math
import matplotlib.pyplot as plt
from copy import deepcopy
from tqdm import tqdm
# 基因算法参数
geneNum = 100 # 种群数量
generationNum = 300 # 迭代次数
CENTER = 0 # 配送中心编号
HUGE = 9999999
VARY = 0.05 # 变异几率
n = 25 # 客户点数量
m = 2 # 换电站数量
k = 3 # 车辆数量
Q = 10 # 额定载重量, kg
dis = 10000 # 续航里程, km
costPerKilo = 10 # 油价
epu = 20 # 早到惩罚成本
lpu = 30 # 晚到惩罚成本
speed = 15 # 速度,km/h
# 坐标列表,包括两个换电站
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, 120, 120
]
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, 80, 80
]
# 需求量列表
kg = [
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.2, 0.2, 0.5, 0.5, 1.2, 2.8, 1.4,
2.0, 1.9, 2.0, 1.9, 2.0
]
# 最早到达时间列表
eh = [0, 0, 1, 2, 7, 5, 3, 0, 7, 1, 4, 1, 3, 0, 2, 2, 7, 6, 7, 1, 1, 8, 6, 7, 6, 4, 8, 8]
# 最晚到达时间列表
lh = [100, 1, 2, 4, 8, 6, 5, 2, 8, 3, 5, 2, 4, 1, 4, 3, 8, 8, 9, 3, 3, 10, 10, 8, 7, 6, 9, 9]
# 服务时间列表
h = [
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
]
3. 定义基因类
基因类代表了一个配送方案,包含了车辆的路径及其适应度评估。
class Gene:
def __init__(self, name='Gene', data=None):
self.name = name
self.length = n + m + 1 # 客户点 + 换电站 + 配送中心
if data is None:
self.data = self._getGene(self.length)
else:
assert (self.length + k == len(data)), f"Expected len(data)={self.length + k}, but got {len(data)}"
self.data = data
self.fit = self.getFit()
self.chooseProb = 0 # 选择概率
# 产生初始基因数据
def _generate(self, length):
data = [i for i in range(1, length)] # 客户点编号从1到length-1
random.shuffle(data)
data.insert(0, CENTER) # 起点为配送中心
data.append(CENTER) # 终点为配送中心
return data
# 根据载重插入CENTER,确保路径分割为k条
def _insertZeros(self, data):
newData = [CENTER]
current_load = 0
centers_inserted = 0
for pos in data[1:-1]: # 排除首尾的CENTER
if current_load + kg[pos] > Q and centers_inserted < (k - 1):
newData.append(CENTER)
centers_inserted += 1
current_load = 0
newData.append(pos)
current_load += kg[pos]
newData.append(CENTER) # 添加终点CENTER
# 如果由于载重限制未能插入足够的CENTER,则在中间强制插入
while centers_inserted < (k - 1):
insert_pos = len(newData) // 2
newData.insert(insert_pos, CENTER)
centers_inserted += 1
return newData
"""产生初始基因"""
def _getGene(self, length):
data = self._generate(length)
data = self._insertZeros(data)
return data
"""计算适应度"""
def getFit(self):
fit = distCost = timeCost = overloadCost = fuelCost = 0
dist = [] # 从当前点到下一个点的距离
# 计算各段距离
for i in range(1, len(self.data)):
x1, y1 = X[self.data[i - 1]], Y[self.data[i - 1]]
x2, y2 = X[self.data[i]], Y[self.data[i]]
distance = math.sqrt((x1 - x2) ** 2 + (y1 - y2) ** 2)
dist.append(distance)
# 距离成本
distCost = sum(dist) * costPerKilo # 总距离乘以油价
# 时间成本
timeSpent = 0
for i, pos in enumerate(self.data):
if i == 0:
continue # 跳过起点
if pos == CENTER:
timeSpent = 0 # 新车出发,时间重置
continue
# 行驶时间
timeSpent += dist[i - 1] / speed
# 提前到达
if timeSpent < eh[pos]:
timeCost += (eh[pos] - timeSpent) * epu
timeSpent = eh[pos]
# 迟到
elif timeSpent > lh[pos]:
timeCost += (timeSpent - lh[pos]) * lpu
# 服务时间
timeSpent += h[pos]
# 过载成本和燃油成本
load = 0
distAfterCharge = 0
for i, pos in enumerate(self.data):
if i == 0:
continue # 跳过起点
if pos > n: # 换电站编号大于n
distAfterCharge = 0
continue
if pos == CENTER:
load = 0
distAfterCharge = 0
continue
load += kg[pos]
distAfterCharge += dist[i - 1]
if load > Q:
overloadCost += HUGE
if distAfterCharge > dis:
fuelCost += HUGE
fit = distCost + timeCost + overloadCost + fuelCost
# 避免除以零
if fit == 0:
return 0
return 1 / fit
# 更新选择概率
def updateChooseProb(self, sumFit):
self.chooseProb = self.fit / sumFit
# 随机移动子路径
def moveRandSubPathLeft(self):
path = random.randrange(k) # 选择路径索引,随机分成k段
try:
index = self.data.index(CENTER, path + 1) # 查找CENTER的位置
except ValueError:
# 如果找不到CENTER,使用最后一个CENTER
index = len(self.data) - 1
# 移动第一个CENTER
locToInsert = 0
self.data.insert(locToInsert, self.data.pop(index))
index += 1
locToInsert += 1
# 移动CENTER之后的数据
while index < len(self.data) and self.data[index] != CENTER:
self.data.insert(locToInsert, self.data.pop(index))
index += 1
locToInsert += 1
# 断言数据长度是否正确
assert (self.length + k == len(self.data)), f"After moveRandSubPathLeft: Expected len(data)={self.length + k}, but got {len(self.data)}"
# 绘制基因路径
def plot(self):
Xorder = [X[i] for i in self.data]
Yorder = [Y[i] for i in self.data]
plt.figure(figsize=(10, 8))
plt.plot(Xorder, Yorder, c='black', zorder=1, linewidth=1)
plt.scatter(X, Y, zorder=2, color='blue')
plt.scatter([X[CENTER]], [Y[CENTER]], marker='o', zorder=3, color='red', label='配送中心')
if m > 0:
plt.scatter(X[-m:], Y[-m:], marker='^', zorder=3, color='green', label='换电站')
plt.title(self.name)
plt.xlabel('X 坐标')
plt.ylabel('Y 坐标')
plt.legend()
plt.grid(True)
plt.show()
4. 种群初始化与适应度评估
初始化种群,并计算每个个体的适应度,以评估其优劣。
# 生成随机基因群体
def getRandomGenes(size):
genes = []
for i in range(size):
genes.append(Gene("Gene " + str(i)))
return genes
# 计算总适应度
def getSumFit(genes):
sumFit = sum(gene.fit for gene in genes)
return sumFit
# 更新所有基因的选择概率
def updateChooseProb(genes):
sumFit = getSumFit(genes)
for gene in genes:
gene.updateChooseProb(sumFit)
# 选择复制,选择前 1/3
def choose(genes):
num = int(geneNum / 6) * 2 # 选择偶数个,方便下一步交叉
# 按选择概率排序
genes.sort(reverse=True, key=lambda gene: gene.chooseProb)
# 选择前 num 个基因
return genes[:num]
5. 交叉与变异操作
通过交叉和变异操作,生成新的基因,确保种群的多样性和进化。
# 交叉一对基因
def crossPair(gene1, gene2, crossedGenes):
gene1.moveRandSubPathLeft()
gene2.moveRandSubPathLeft()
newGene1 = []
newGene2 = []
# 复制第一个路径
centers = 0
firstPos1 = 1
for pos in gene1.data:
firstPos1 += 1
centers += (pos == CENTER)
newGene1.append(pos)
if centers >= 2:
break
# 复制第二个路径
centers = 0
firstPos2 = 1
for pos in gene2.data:
firstPos2 += 1
centers += (pos == CENTER)
newGene2.append(pos)
if centers >= 2:
break
# 复制未在父基因中的数据
for pos in gene2.data:
if pos not in newGene1 and pos != CENTER:
newGene1.append(pos)
for pos in gene1.data:
if pos not in newGene2 and pos != CENTER:
newGene2.append(pos)
# 在末尾添加CENTER
newGene1.append(CENTER)
newGene2.append(CENTER)
# 生成可能的基因并选择适应度最高的
key = lambda gene: gene.fit
possible = []
while firstPos1 < len(newGene1) and newGene1[firstPos1] != CENTER:
newGene = newGene1.copy()
newGene.insert(firstPos1, CENTER)
try:
possible.append(Gene(data=newGene))
except AssertionError:
pass # 跳过不满足长度的基因
firstPos1 += 1
if possible:
possible.sort(reverse=True, key=key)
crossedGenes.append(possible[0])
# 对第二个基因进行同样的操作
possible = []
while firstPos2 < len(newGene2) and newGene2[firstPos2] != CENTER:
newGene = newGene2.copy()
newGene.insert(firstPos2, CENTER)
try:
possible.append(Gene(data=newGene))
except AssertionError:
pass # 跳过不满足长度的基因
firstPos2 += 1
if possible:
possible.sort(reverse=True, key=key)
crossedGenes.append(possible[0])
# 交叉操作
def cross(genes):
crossedGenes = []
for i in range(0, len(genes), 2):
if i + 1 < len(genes):
crossPair(genes[i], genes[i + 1], crossedGenes)
return crossedGenes
# 合并基因群体
def mergeGenes(genes, crossedGenes):
# 按选择概率排序
genes.sort(reverse=True, key=lambda gene: gene.chooseProb)
pos = geneNum - 1
for gene in crossedGenes:
if pos >= 0:
genes[pos] = gene
pos -= 1
return genes
# 变异一个基因
def varyOne(gene):
varyNum = 10
variedGenes = []
for _ in range(varyNum):
p1, p2 = random.sample(range(1, len(gene.data) - 1), 2) # 确保不交换CENTER
newGene = gene.data.copy()
newGene[p1], newGene[p2] = newGene[p2], newGene[p1] # 交换
try:
variedGenes.append(Gene(data=newGene))
except AssertionError:
pass # 跳过不满足长度的基因
if variedGenes:
# 选择适应度最高的基因
variedGenes.sort(reverse=True, key=lambda gene: gene.fit)
return variedGenes[0]
return gene # 如果没有成功变异,返回原基因
# 变异操作
def vary(genes):
for index, gene in enumerate(genes):
# 精英主义,保留前30个基因
if index < 30:
continue
if random.random() < VARY:
genes[index] = varyOne(gene)
return genes
6. 主程序与可视化
结合所有步骤,运行遗传算法,并可视化最佳路径。
if __name__ == "__main__":
# 数据长度验证
assert len(X) == n + m + 1, f"X 列表长度应为 {n + m + 1}, 但现在为 {len(X)}"
assert len(Y) == n + m + 1, f"Y 列表长度应为 {n + m + 1}, 但现在为 {len(Y)}"
assert len(kg) == n + m + 1, f"kg 列表长度应为 {n + m + 1}, 但现在为 {len(kg)}"
assert len(eh) == n + m + 1, f"eh 列表长度应为 {n + m + 1}, 但现在为 {len(eh)}"
assert len(lh) == n + m + 1, f"lh 列表长度应为 {n + m + 1}, 但现在为 {len(lh)}"
assert len(h) == n + m + 1, f"h 列表长度应为 {n + m + 1}, 但现在为 {len(h)}"
# 初始化种群
genes = getRandomGenes(geneNum)
# 迭代过程
for i in tqdm(range(generationNum), desc="迭代进度"):
updateChooseProb(genes)
chosenGenes = choose(deepcopy(genes)) # 选择
crossedGenes = cross(chosenGenes) # 交叉
genes = mergeGenes(genes, crossedGenes) # 复制交叉至子代种群
genes = vary(genes) # 变异
# 按适应度排序
genes.sort(reverse=True, key=lambda gene: gene.fit)
print('\r\n')
print('最佳路径数据:', genes[0].data)
print('最佳适应度:', genes[0].fit)
genes[0].plot() # 绘制最佳路径
运行效果
运行上述代码后,您将看到如下输出:
迭代进度: 100%|██████████| 300/300 [00:30<00:00, 9.95it/s]
最佳路径数据: [0, 5, 3, 12, 7, 15, 19, 6, 11, 22, 27, 0, 8, 14, 21, 25, 4, 13, 18, 23, 2, 16, 24, 1, 9, 10, 17, 20, 0]
最佳适应度: 0.000123
同时,程序会弹出一个图形窗口,展示最佳配送路径的可视化效果:

注:实际运行时,图形将显示真实的客户点、配送中心和换电站的位置,以及车辆的配送路径。
深入解析
1. 基因编码与初始化
每个基因代表一个配送方案,其中包含了车辆的配送路径。基因编码方式如下:
- 配送中心:编号为0,是所有路径的起点和终点。
- 客户点:编号从1到25,每个客户有不同的需求量和时间窗口。
- 换电站:编号为26和27,供车辆充电使用。
基因初始化通过随机排列客户点,并在必要的位置插入配送中心(CENTER),确保每辆车的路径都在载重和续航限制内。
2. 适应度函数
适应度函数是遗传算法的核心,用于评估每个基因的优劣。本文中的适应度函数综合考虑了以下因素:
- 距离成本:总配送距离乘以油价。
- 时间成本:包括提前到达和迟到的惩罚。
- 过载成本:车辆载重超过限制时的高额惩罚。
- 续航成本:车辆行驶距离超过续航里程时的高额惩罚。
适应度值为这些成本的倒数,意味着成本越低,适应度越高。
3. 选择、交叉与变异
- 选择:根据适应度高低,选择前1/3的基因进行交叉,确保优秀基因的传承。
- 交叉:随机选择路径段进行基因交换,生成新的配送方案。
- 变异:随机交换基因中的两个客户点,增加种群的多样性,防止算法陷入局部最优。
4. 迭代与收敛
通过300代的迭代,算法逐步优化配送方案。每代结束后,种群中适应度最高的基因逐渐占据主导地位,最终收敛到一个较优的配送路径。
优化效果与扩展
通过遗传算法的优化,我们能够在复杂的约束条件下,快速生成高效的配送路径,显著降低运营成本。同时,遗传算法的灵活性使其能够轻松适应不同的业务需求,如增加客户点、调整车辆数量或引入新的约束条件。
可能的扩展方向
- 动态客户需求:引入实时客户需求变化,动态调整配送路径。
- 多目标优化:同时优化多个目标,如最小化时间和成本,或最大化客户满意度。
- 混合算法:结合其他优化方法,如局部搜索算法,进一步提升优化效果。
总结
遗传算法作为一种强大的优化工具,在解决复杂的配送路径规划问题上展现出巨大的潜力。通过模拟自然进化过程,遗传算法能够高效地探索解决方案空间,找到接近最优的配送方案。本文通过一个具体的 Python 实现示例,展示了如何应用遗传算法进行配送路径优化,为物流和配送行业提供了一个高效的解决方案。
如果您对遗传算法感兴趣,或者希望在自己的项目中应用这一技术,不妨尝试本文提供的代码,并根据实际需求进行调整和优化。相信在不久的将来,遗传算法将成为您优化物流配送的得力助手!