优化配送路径:使用遗传算法的 Python 实现

在现代物流和配送系统中,如何高效地规划车辆路径以满足客户需求,同时最小化成本和时间,是一个备受关注的问题。传统的优化方法在面对复杂约束时往往捉襟见肘,而遗传算法(Genetic Algorithms, GA)作为一种强大的进化计算技术,为解决此类问题提供了新的思路。本文将带您深入了解如何使用 Python 实现遗传算法来优化配送路径。

背景介绍

设想您是一家配送公司的运营经理,负责规划多辆配送车辆的路径,以确保所有客户订单能够准时送达,同时最小化油费、时间成本和过载成本。这个问题涉及多个变量和约束,如客户分布、车辆载重、续航里程以及时间窗口等,传统的优化方法难以在合理时间内找到最佳解。这时,遗传算法便成为一种理想的解决方案。

什么是遗传算法?

遗传算法是一种模拟自然选择和遗传机制的优化方法。它通过种群的进化过程,不断生成新一代的解,并通过选择、交叉和变异等操作,逐步逼近问题的最优解。其核心思想包括:

  1. 种群初始化:随机生成一组初始解(称为个体或基因)。
  2. 适应度评估:评估每个个体的优劣,适应度越高,表示该解越优。
  3. 选择:根据适应度选择优秀个体进行繁殖。
  4. 交叉:通过交换父代基因生成新的子代。
  5. 变异:随机改变子代基因,增加种群的多样性。
  6. 迭代:重复上述过程,直到满足停止条件(如达到最大迭代次数或找到满意解)。

问题描述

在本案例中,我们需要解决以下配送路径优化问题:

  • 配送中心:所有配送路径的起点和终点。
  • 客户点: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代的迭代,算法逐步优化配送方案。每代结束后,种群中适应度最高的基因逐渐占据主导地位,最终收敛到一个较优的配送路径。

优化效果与扩展

通过遗传算法的优化,我们能够在复杂的约束条件下,快速生成高效的配送路径,显著降低运营成本。同时,遗传算法的灵活性使其能够轻松适应不同的业务需求,如增加客户点、调整车辆数量或引入新的约束条件。

可能的扩展方向

  1. 动态客户需求:引入实时客户需求变化,动态调整配送路径。
  2. 多目标优化:同时优化多个目标,如最小化时间和成本,或最大化客户满意度。
  3. 混合算法:结合其他优化方法,如局部搜索算法,进一步提升优化效果。

总结

遗传算法作为一种强大的优化工具,在解决复杂的配送路径规划问题上展现出巨大的潜力。通过模拟自然进化过程,遗传算法能够高效地探索解决方案空间,找到接近最优的配送方案。本文通过一个具体的 Python 实现示例,展示了如何应用遗传算法进行配送路径优化,为物流和配送行业提供了一个高效的解决方案。

如果您对遗传算法感兴趣,或者希望在自己的项目中应用这一技术,不妨尝试本文提供的代码,并根据实际需求进行调整和优化。相信在不久的将来,遗传算法将成为您优化物流配送的得力助手!

相关推荐
databook8 小时前
Manim实现闪光轨迹特效
后端·python·动效
Juchecar9 小时前
解惑:NumPy 中 ndarray.ndim 到底是什么?
python
用户8356290780519 小时前
Python 删除 Excel 工作表中的空白行列
后端·python
Json_9 小时前
使用python-fastApi框架开发一个学校宿舍管理系统-前后端分离项目
后端·python·fastapi
数据智能老司机16 小时前
精通 Python 设计模式——分布式系统模式
python·设计模式·架构
数据智能老司机17 小时前
精通 Python 设计模式——并发与异步模式
python·设计模式·编程语言
数据智能老司机17 小时前
精通 Python 设计模式——测试模式
python·设计模式·架构
数据智能老司机17 小时前
精通 Python 设计模式——性能模式
python·设计模式·架构
c8i17 小时前
drf初步梳理
python·django
每日AI新事件17 小时前
python的异步函数
python