遗传算法实践(八) 简单配送问题

简单配送问题描述

基本配送计划模型是从产地i=1,2,...,I往销地j=1,2,...,J配送产品时,在满足产地的供应量和销地的需求量这一约束前提下,制定配送总成本最低的配送计划。即
min \ z = \sum_{i=1}^{I}\sum_{j=1}^{J}c_{ij}x_{ij} \\ s.t. \sum_{j=1}^J x_{ij} \le a_i,\ i=1,2,3,...,I \\ \sum_{i=1}^Ix_{ij}\ge b_j,\ j=1,2,...,J \\ x_{ij}\ge0,\ \forall i,j \\ \sum_{i=1}^I a_i=\sum_{j=1}^J b_j

简单配送问题示例

考虑有4个产地和5个销地的配送计划问题,供应量和需求量如下:
a_1=8, a_2=4, a_3=12, a_4=6 \\ b_1=3, b_2=5, b_3=10, b_4=7, b_5=5

产地和销地之间每单位数量商品的运输成本为:

销地1 销地2 销地3 销地4 销地5
产地1 8.6860922 9.35319846 4.04839742 6.64822502 2.21060537
产地2 2.09958268 7.93166898 5.69745574 4.18627465 6.11192203
产地3 7.05011006 9.65028246 5.79551805 2.74632325 5.96366128
产地4 6.66228395 8.73934611 4.89579209 6.13292158 5.65538837

遗传算法求解简单配送问题

个体编码

这里采用基于优先级的编码方式。

设有I个产地,J个销地,b_i为产地i的供应,d_j为销地j的需求,c_{ij}为从产地i运输单位数量商品到销地j的代价,y_{ij}为从产地i运往销地j的商品数量。

染色体编码v长度为I+J,前I个位置代表产地,后J个位置代表销地,其编码过程如下:

  • Step 0: 初始化,优先级初始化为p\leftarrow (I+J)v_t\leftarrow 0,\forall t\in(I+J)
  • Step 1: 从供应量和需求量中有一个不为0的产-销地组合中,找到运输代价最小的,并为其分配最大运输量(该组合对应的供应量和需求量中较小者),(i^\prime,j^\prime)\leftarrow argmin\{(c_{ij}|y_{ij}\ne 0), (b_{i^\prime}=y_{ij}||d_{j^\prime}=y_{ij})\}
  • Step 2: 更新供应量和需求量,b_{i^\prime}=b_{i^\prime}-y_{i^\prime j^\prime}d_{j^\prime}=d_{j^\prime}-y_{i^\prime j^\prime}
  • Step 3: 根据更新后的供应量和需求量分配优先级

if\ b_{i^\prime}=0, then\ v_{i^\prime}\leftarrow p, p\leftarrow p-1; \\if\ d_{j^\prime}=0, then\ v_{I+j^\prime}\leftarrow p, p\leftarrow p-1;

  • Step 4: 重复Step 1-3直到(b_{i^\prime}=0,\ \forall i^\prime \in I)\&(d_{j^\prime}=0,\ \forall j^\prime \in J)
  • Step 5: 为染色体中仍然为0的剩余位置随机填充优先值

解码

解码过程如下:

  • Step 0: 初始化各产销地配对的运输量y_{ij}\leftarrow 0, \forall i\in I, j\in J
  • Step 1: 选择优先度最高的节点l\leftarrow argmax\{v\}
  • Step 2: 如果该节点是销地,则从产地选择运输代价最小的节点if\ l\in J, then\ j\leftarrow li*\leftarrow argmin\{ c_{il}|v_i\ne 0, i\in I\};如果该节点是产地,则从销地选择运输代价最小的节点Else, \ i\leftarrow lj*\leftarrow argmin\{ c_{lj}|v_j\ne 0, j\in J\}
  • Step 3:为选择的产-销地更新可用的最大运输量y_{i*j*}\leftarrow min\{b_{i*},d_{j*}\}
  • Step 4:更新剩余的需求量和销量b_i\leftarrow b_i-y_{i*j*}d_j\leftarrow d_j - y_{i*j*}
  • Step 5: 如果完成了需求量和销量的分配,将优先度归零。if\ b_{i*}=0,then\ v_{i*}\leftarrow 0if\ d_{j*}=0,then\ v_{I+j*}\leftarrow 0
  • 重复Step 1-5直到优先度编码全部归零。

交叉操作

交叉操作采用PMX交叉

突变操作

使用shuffleIndex突变

代码示例

完整代码如下:

## 环境设置
import numpy as np
import matplotlib.pyplot as plt
from deap import base, tools, creator, algorithms
import random

params = {
    'font.family': 'serif',
    'figure.dpi': 300,
    'savefig.dpi': 300,
    'font.size': 12,
    'legend.fontsize': 'small'
}
plt.rcParams.update(params)

from copy import deepcopy
## 问题描述
creator.create('FitnessMin', base.Fitness, weights=(-1.0,))
creator.create('Individual', list, fitness=creator.FitnessMin)

## 个体编码
sourceFlow = [8, 4, 12, 6]
destinationFlow = [3, 5, 10, 7, 5]

def matEncode(sourceFlow=sourceFlow, destinationFlow=destinationFlow):
    '''生成可行的配送量矩阵'''
    rowNum = len(sourceFlow)
    colNum = len(destinationFlow)
    posSet = list(range(rowNum * colNum))  # 当前未填充的位置集合
    mat = np.zeros((rowNum, colNum))  # 初始化染色体矩阵
    sourceFlowCopy = deepcopy(sourceFlow)
    destinationFlowCopy = deepcopy(destinationFlow)
    while posSet:
        randIdx = random.randint(0, len(posSet)-1)  # 随机选取位置集中的一个元素
        posNum = posSet[randIdx]
        rowPos = (posNum - 1)//colNum + 1  # 第rowPos行
        colPos = posNum % colNum  # 第colPos列
        # 填充矩阵相应位置
        mat[rowPos-1, colPos -
            1] = min(sourceFlowCopy[rowPos-1], destinationFlowCopy[colPos-1])
        # 根据填充值,修改剩余的产量和销量
        sourceFlowCopy[rowPos-1] -= mat[rowPos-1, colPos-1]
        destinationFlowCopy[colPos-1] -= mat[rowPos-1, colPos-1]
        # 从位置集合中删去已经被填充的位置
        posSet = posSet[:randIdx] + posSet[randIdx+1:]
    return mat.tolist()

# Cost Matrix
costMat = [
    [8.6860922, 9.35319846, 4.04839742, 6.64822502, 2.21060537],
    [2.09958268, 7.93166898, 5.69745574, 4.18627465, 6.11192203],
    [7.05011006, 9.65028246, 5.79551805, 2.74632325, 5.96366128],
    [6.66228395, 8.73934611, 4.89579209, 6.13292158, 5.65538837]
]

## 基于优先级的编码
def priorityCoding(matCode, costMat=costMat, sourceFlow=sourceFlow, destinationFlow=destinationFlow):
    '''
    从给定的配送量矩阵和代价矩阵,生成基于优先级的编码
    输入: matCode -- 一个满足sourceFlow和destinationFlow的可行解
    costMat -- 代价矩阵,给出由一个source到一个destination的代价
    sourceFlow, destinationFlow -- 每个source的输出能力和destination的接受能力
    输出: 长度为len(sourceFlow) + len(destinationFlow)的编码
    '''
    priorityCode = [0] * (len(sourceFlow) + len(destinationFlow)) # 初始化编码
    priority = len(priorityCode) # 初始化优先级
    # 复制矩阵,防止改变原值
    sourceFlowCopy = deepcopy(sourceFlow)
    destinationFlowCopy = deepcopy(destinationFlow)
    matCodeCopy = deepcopy(matCode)
    costMatCopy = deepcopy(costMat)
    largeNum = 1e5 # 设定一个足够大的数
    # 当流量没有完全分配时,执行迭代
    while not (np.any(sourceFlowCopy) and np.any(destinationFlowCopy)):
        # 为配送量为0的连接分配较大代价
        costMatCopy = np.where(matCodeCopy==0, largeNum, costMatCopy)
        i,j = np.where(costMatCopy = np.min(costMatCopy)) # 最小运输代价所对应的source和destination
        # 更新剩余source和destination流量
        sourceFlowCopy[i] -= matCodeCopy[i][j]
        destinationFlowCopy[j] -= matCodeCopy[i][j]
        # 当剩余sourceFlow或者destinationFlow为0时,分配优先度编码
        if sourceFlowCopy[i] == 0:
            priorityCode[i] = priority
            priority -= 1
        if destinationFlowCopy[j] == 0:
            priorityCode[j + len(sourceFlowCopy)] = priority
            priority -= 1
    # 为剩余位置填充优先级,因为该边上实质流量为0,因此事实上非有效边,可以随意分配流量
    perm = np.random.permutation(range(1, priority + 1))
    priorityCode = np.asarray(priorityCode)
    priorityCode[priorityCode==0] = perm
    return priorityCode


## 解码
def priorityDecoding(priorityCode, costMat=costMat, sourceFlow=sourceFlow, destinationFlow=destinationFlow):
    '''根据优先度编码解码回配送方案的矩阵编码
    输入:priorityCode -- 基于优先级的编码, 长度为len(sourceFlow) + len(destinationFlow)
    costMat -- 代价矩阵,给出由一个source到一个destination的代价
    sourceFlow, destinationFlow -- 每个source的输出能力和destination的接受能力
    输出:matCode -- 一个满足sourceFlow和destinationFlow的可行解矩阵编码'''
    # 初始化矩阵编码
    matCode = np.zeros((len(sourceFlow), len(destinationFlow)))
    # 复制矩阵,防止改变原值
    sourceFlowCopy = deepcopy(sourceFlow)
    destinationFlowCopy = deepcopy(destinationFlow)
    costMatCopy = np.array(deepcopy(costMat))
    priorityCodeCopy = deepcopy(priorityCode)
    largeNum = 1e5 # 设定一个足够大的数
    # 列出source Node和destination Node
    sourceNodeList = list(range(1, len(sourceFlow)+1))
    destinationNodeList = list(range(len(sourceFlow)+1,
                               len(destinationFlow)+len(sourceFlow)+1))
    nodeList = sourceNodeList + destinationNodeList
    while np.any(priorityCodeCopy):
        # 选择优先度最高的节点    
        nodeSelected = np.asarray(nodeList)[np.argmax(priorityCodeCopy)]
        # 为剩余流量为0的行和列分配一个大数作为运输代价
        rowIdx = [i for i in range(len(sourceFlowCopy)) if sourceFlowCopy[i]==0]
        colIdx = [i for i in range(len(destinationFlowCopy)) if destinationFlowCopy[i]==0]
        for row in rowIdx:
            costMatCopy[row,:] = [largeNum]*len(costMatCopy[row,:])
        for col in colIdx:
            costMatCopy[:,col] = [largeNum]*len(costMatCopy[:,col])
        # 如果选中的节点在供方,则从需方选择对应运输代价最小的节点
        if nodeSelected in sourceNodeList:
            sourceNodeIdx = nodeSelected -1 # 作为index,比节点标号小1
            destinationNodeIdx = np.argmin(costMatCopy[sourceNodeIdx,:])
        # 如果选中的节点在需方,则从供方选择对应运输代价最小的节点
        else:
            destinationNodeIdx = nodeSelected -1 - len(sourceFlow)
            sourceNodeIdx = np.argmin(costMatCopy[:,destinationNodeIdx])
        # 更新选中边上的流量
        matCode[sourceNodeIdx][destinationNodeIdx] = min(sourceFlowCopy[sourceNodeIdx],
                                                        destinationFlowCopy[destinationNodeIdx])
        # 更新剩余流量
        sourceFlowCopy[sourceNodeIdx] -= matCode[sourceNodeIdx][destinationNodeIdx]
        destinationFlowCopy[destinationNodeIdx] -= matCode[sourceNodeIdx][destinationNodeIdx]
        # 更新优先度
        if sourceFlowCopy[sourceNodeIdx] == 0:
            priorityCodeCopy[sourceNodeIdx] = 0
        if destinationFlowCopy[destinationNodeIdx] == 0:
            priorityCodeCopy[destinationNodeIdx + len(sourceFlowCopy)] = 0
    return matCode

## 评价函数
def evaluate(ind):
    matCode = priorityDecoding(ind) # 将个体优先度编码解码为矩阵编码
    return np.sum(np.sum(np.asarray(costMat)*np.asarray(matCode))),

## DEAP原生的PMX需要个体编码从0开始
def PMX(ind1, ind2):
    ind1Aligned = [ind1[i]-1 for i in range(len(ind1))]
    ind2Aligned = [ind2[i]-1 for i in range(len(ind2))]
    tools.cxPartialyMatched(ind1Aligned, ind2Aligned)
    ind1Recovered = [ind1Aligned[i]+1 for i in range(len(ind1Aligned))]
    ind2Recovered = [ind2Aligned[i]+1 for i in range(len(ind2Aligned))]
    ind1[:] = ind1Recovered
    ind2[:] = ind2Recovered
    return ind1,ind2

## 注册工具
toolbox = base.Toolbox()
toolbox.register('genPriority', priorityCoding, matEncode())
toolbox.register('individual', tools.initIterate, creator.Individual, toolbox.genPriority)
toolbox.register('evaluate', evaluate)
toolbox.register('select', tools.selTournament, tournsize=2)
toolbox.register('mate', PMX)
toolbox.register('mutate', tools.mutShuffleIndexes, indpb=0.5)

## 生成初始族群
toolbox.popSize = 100
toolbox.register('population', tools.initRepeat, list, toolbox.individual)
pop = toolbox.population(toolbox.popSize)

## 记录迭代数据
stats=tools.Statistics(key=lambda ind: ind.fitness.values)
stats.register('min', np.min)
stats.register('avg', np.mean)
stats.register('std', np.std)
hallOfFame = tools.HallOfFame(maxsize=1)

## 遗传算法参数
toolbox.ngen = 500
toolbox.cxpb = 0.8
toolbox.mutpb = 0.1


# ## 遗传算法主程序
pop,logbook=algorithms.eaSimple(pop, toolbox, cxpb=toolbox.cxpb, mutpb=toolbox.mutpb,
                   ngen=toolbox.ngen ,stats=stats, halloffame=hallOfFame, verbose=True)

计算结果如下:

# 输出结果
from pprint import pprint
bestInd = hallOfFame.items[0]
bestFitness = bestInd.fitness.values
print('最佳运输组合为:')
pprint(priorityDecoding(bestInd))
print('该运输组合的代价为:'+str(bestFitness))

# 画出迭代图
minFit = logbook.select('min')
avgFit = logbook.select('avg')
plt.plot(minFit, 'b-', label='Minimum Fitness')
plt.plot(avgFit, 'r-', label='Average Fitness')
plt.xlabel('# Gen')
plt.ylabel('Fitness')
plt.legend(loc='best')
# 计算结果:
#最佳运输组合为:
#array([[0., 0., 3., 0., 5.],
#       [3., 1., 0., 0., 0.],
#       [0., 0., 5., 7., 0.],
#       [0., 4., 2., 0., 0.]])
#该运输组合的代价为:(130.37945775,)

迭代过程如图所示:

Tansportation planning_iterations
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,423评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,147评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,019评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,443评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,535评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,798评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,941评论 3 407
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,704评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,152评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,494评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,629评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,295评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,901评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,978评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,333评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,499评论 2 348

推荐阅读更多精彩内容