优化配送路径:使用遗传算法的 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 实现示例,展示了如何应用遗传算法进行配送路径优化,为物流和配送行业提供了一个高效的解决方案。

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

相关推荐
Hylan_J3 小时前
【VSCode】MicroPython环境配置
ide·vscode·python·编辑器
软件黑马王子3 小时前
C#初级教程(4)——流程控制:从基础到实践
开发语言·c#
莫忘初心丶3 小时前
在 Ubuntu 22 上使用 Gunicorn 启动 Flask 应用程序
python·ubuntu·flask·gunicorn
闲猫3 小时前
go orm GORM
开发语言·后端·golang
李白同学5 小时前
【C语言】结构体内存对齐问题
c语言·开发语言
黑子哥呢?6 小时前
安装Bash completion解决tab不能补全问题
开发语言·bash
失败尽常态5236 小时前
用Python实现Excel数据同步到飞书文档
python·excel·飞书
2501_904447746 小时前
OPPO发布新型折叠屏手机 起售价8999
python·智能手机·django·virtualenv·pygame
青龙小码农6 小时前
yum报错:bash: /usr/bin/yum: /usr/bin/python: 坏的解释器:没有那个文件或目录
开发语言·python·bash·liunx
大数据追光猿6 小时前
Python应用算法之贪心算法理解和实践
大数据·开发语言·人工智能·python·深度学习·算法·贪心算法