遺傳算法實(shí)踐(九) VRP問題

VRP(Vehicle Routing Problem)問題描述

在VRP問題中,假設(shè)有一個(gè)供求關(guān)系系統(tǒng)血当,車輛從倉(cāng)庫(kù)取貨翘贮,配送到若干個(gè)顧客處赊窥。車輛受到載重量的約束,需要組織適當(dāng)?shù)男熊嚶肪€狸页,在顧客的需求得到滿足的基礎(chǔ)上锨能,使代價(jià)函數(shù)最小。代價(jià)函數(shù)根據(jù)問題不同而不同芍耘,常見的有車輛總運(yùn)行時(shí)間最小址遇,車輛總運(yùn)行路徑最短等。

這個(gè)問題基于以下假設(shè):

  • 所有距離采用歐幾里得表示
  • 每個(gè)顧客只接受一輛車的一次配送
  • 從配送中心出發(fā)的車輛必須返回配送中心
  • 每輛車載重量相同
  • 一條路徑上顧客的總需求量不能超過車輛的最大載重量

定義j,k為需要服務(wù)的兩個(gè)顧客編號(hào)齿穗,e為配送中心的車輛編號(hào)傲隶,C為顧客和倉(cāng)庫(kù)的集合饺律。
參數(shù):

c_{jk}: 從顧客j到顧客k的行駛距離

d_j:顧客j的需求量

Y_e:車輛的最大載重量

決策變量:

z_{ejk}:當(dāng)車輛e被分配從顧客i運(yùn)行到顧客j時(shí)窃页,取1;否則取0

在給定了參數(shù)和定義了決策變量之后复濒,VRP問題可以用數(shù)學(xué)模型表示為:
min\sum_{e\in E}\sum_{j\in C}\sum_{k \in C}c_{jk}z_{ejk} \\s.t.\begin{aligned} \sum_{e\in E}\sum_{j\in C}z_{ejk} = 1, \forall j,k \in C \backslash \{0\} \\ \sum_{j\in C\backslash \{0\}}\sum_{k\in C\backslash \{0\}}d_k z_{ejk} \le Y_e, \forall i,e \in E \\ \sum_{k\in C}z_{e0k}=1, \forall e \in E \\ \sum_{j\in C\backslash \{0\}}z_{ejk} - \sum_{k\in C\backslash \{0\}}z_{ejk}=0, \forall e \in E \\ \sum_{j\in C_i}z_{ejk}=1, \forall e\in E \\ z_{ejk} = 0 or 1, \forall e \in E, \forall j,k \in C \end{aligned}

VRP問題示例

給定車輛負(fù)載為400脖卖,各個(gè)節(jié)點(diǎn)的坐標(biāo)和需求如下(節(jié)點(diǎn)0為配送中心):

節(jié)點(diǎn)編號(hào)i 節(jié)點(diǎn)坐標(biāo)(x,y) 節(jié)點(diǎn)需求q
0 (15, 12) 0
1 (3, 13) 50
2 (3, 17) 50
3 (6, 18) 60
4 (8, 17) 30
5 (10, 14) 90
6 (14, 13) 10
7 (15, 11) 20
8 (15, 15) 10
9 (17, 11) 30
10 (17, 16) 20
11 (18, 19) 30
12 (19, 9) 10
13 (19, 21) 10
14 (21, 22) 10
15 (23, 9) 40
16 (23, 22) 51
17 (24, 11) 20
18 (27, 21) 20
19 (26, 6) 20
20 (26, 9) 30
21 (27, 2) 30
22 (27, 4) 30
23 (27, 17) 10
24 (28, 7) 60
25 (29, 14) 30
26 (29, 18) 20
27 (30, 1) 30
28 (30, 8) 40
29 (30, 15) 20
30 (30, 17) 20

遺傳算法求解VRP問題

個(gè)體編碼

對(duì)于個(gè)體采用自然數(shù)編碼,0代表配送中心巧颈,1--n代表顧客畦木;不同車輛的配送路線之間用0分隔(即每輛車都從倉(cāng)庫(kù)出發(fā));對(duì)于有n個(gè)顧客砸泛,k輛車的VRP問題來說十籍,染色體長(zhǎng)度為n+k+1。

例如配送中心有3輛車為8個(gè)客戶服務(wù)唇礁,一條可能的染色體如下:

0, 7, 0, 1, 2, 3, 5, 0, 8, 4, 6, 0

這條染色體表示的三輛車的行駛路線為:
第一輛車:0-7-0
第二輛車:0-1-2-3-5-0
第三輛車:0-8-4-6-0

解碼

利用分割符0勾栗,還原各條子路徑

交叉操作

參考了大連海事大學(xué)碩士學(xué)位論文《基于電動(dòng)汽車的帶時(shí)間窗的路徑優(yōu)化問題研究》中的交叉操作,生成新的個(gè)體盏筐,具體描述如下圖:

VRP問題交叉方法描述

突變操作

用2-opt算法對(duì)各條子路徑進(jìn)行局部?jī)?yōu)化

代碼示例

## 環(huán)境設(shè)定
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,)) # 最小化問題
# 給個(gè)體一個(gè)routes屬性用來記錄其表示的路線
creator.create('Individual', list, fitness=creator.FitnessMin) 

#-----------------------------------
## 個(gè)體編碼
# 用字典存儲(chǔ)所有參數(shù) -- 配送中心坐標(biāo)围俘、顧客坐標(biāo)、顧客需求琢融、到達(dá)時(shí)間窗口界牡、服務(wù)時(shí)間、車型載重量
dataDict = {}
# 節(jié)點(diǎn)坐標(biāo)漾抬,節(jié)點(diǎn)0是配送中心的坐標(biāo)
dataDict['NodeCoor'] = [(15,12), (3,13), (3,17), (6,18), (8,17), (10,14),
                           (14,13), (15,11), (15,15), (17,11), (17,16),
                            (18,19), (19,9), (19,21), (21,22), (23,9),
                            (23,22), (24,11), (27,21), (26,6), (26,9),
                            (27,2), (27,4), (27,17), (28,7), (29,14),
                            (29,18), (30,1), (30,8), (30,15), (30,17)
                           ]
# 將配送中心的需求設(shè)置為0
dataDict['Demand'] = [0,50,50,60,30,90,10,20,10,30,20,30,10,10,10,
                      40,51,20,20,20,30,30,30,10,60,30,20,30,40,20,20]
dataDict['MaxLoad'] = 400
dataDict['ServiceTime'] = 1

def genInd(dataDict = dataDict):
    '''生成個(gè)體宿亡, 對(duì)我們的問題來說,困難之處在于車輛數(shù)目是不定的'''
    nCustomer = len(dataDict['NodeCoor']) - 1 # 顧客數(shù)量
    perm = np.random.permutation(nCustomer) + 1 # 生成顧客的隨機(jī)排列,注意顧客編號(hào)為1--n
    pointer = 0 # 迭代指針
    lowPointer = 0 # 指針指向下界
    permSlice = []
    # 當(dāng)指針不指向序列末尾時(shí)
    while pointer < nCustomer -1:
        vehicleLoad = 0
        # 當(dāng)不超載時(shí)纳令,繼續(xù)裝載
        while (vehicleLoad < dataDict['MaxLoad']) and (pointer < nCustomer -1):
            vehicleLoad += dataDict['Demand'][perm[pointer]]
            pointer += 1
        if lowPointer+1 < pointer:
            tempPointer = np.random.randint(lowPointer+1, pointer)
            permSlice.append(perm[lowPointer:tempPointer].tolist())
            lowPointer = tempPointer
            pointer = tempPointer
        else:
            permSlice.append(perm[lowPointer::].tolist())
            break
    # 將路線片段合并為染色體
    ind = [0]
    for eachRoute in permSlice:
        ind = ind + eachRoute + [0]
    return ind
#-----------------------------------
## 評(píng)價(jià)函數(shù)
# 染色體解碼
def decodeInd(ind):
    '''從染色體解碼回路線片段她混,每條路徑都是以0為開頭與結(jié)尾'''
    indCopy = np.array(deepcopy(ind)) # 復(fù)制ind烈钞,防止直接對(duì)染色體進(jìn)行改動(dòng)
    idxList = list(range(len(indCopy)))
    zeroIdx = np.asarray(idxList)[indCopy == 0]
    routes = []
    for i,j in zip(zeroIdx[0::], zeroIdx[1::]):
        routes.append(ind[i:j]+[0])
    return routes

def calDist(pos1, pos2):
    '''計(jì)算距離的輔助函數(shù),根據(jù)給出的坐標(biāo)pos1和pos2坤按,返回兩點(diǎn)之間的距離
    輸入: pos1, pos2 -- (x,y)元組
    輸出: 歐幾里得距離'''
    return np.sqrt((pos1[0] - pos2[0])*(pos1[0] - pos2[0]) + (pos1[1] - pos2[1])*(pos1[1] - pos2[1]))

#
def loadPenalty(routes):
    '''輔助函數(shù)毯欣,因?yàn)樵诮徊婧屯蛔冎锌赡軙?huì)產(chǎn)生不符合負(fù)載約束的個(gè)體,需要對(duì)不合要求的個(gè)體進(jìn)行懲罰'''
    penalty = 0
    # 計(jì)算每條路徑的負(fù)載臭脓,取max(0, routeLoad - maxLoad)計(jì)入懲罰項(xiàng)
    for eachRoute in routes:
        routeLoad = np.sum([dataDict['Demand'][i] for i in eachRoute])
        penalty += max(0, routeLoad - dataDict['MaxLoad'])
    return penalty

def calRouteLen(routes,dataDict=dataDict):
    '''輔助函數(shù)酗钞,返回給定路徑的總長(zhǎng)度'''
    totalDistance = 0 # 記錄各條路線的總長(zhǎng)度
    for eachRoute in routes:
        # 從每條路徑中抽取相鄰兩個(gè)節(jié)點(diǎn),計(jì)算節(jié)點(diǎn)距離并進(jìn)行累加
        for i,j in zip(eachRoute[0::], eachRoute[1::]):
            totalDistance += calDist(dataDict['NodeCoor'][i], dataDict['NodeCoor'][j])    
    return totalDistance

def evaluate(ind):
    '''評(píng)價(jià)函數(shù)来累,返回解碼后路徑的總長(zhǎng)度砚作,'''
    routes = decodeInd(ind) # 將個(gè)體解碼為路線
    totalDistance = calRouteLen(routes)
    return (totalDistance + loadPenalty(routes)),
#-----------------------------------
## 交叉操作
def genChild(ind1, ind2, nTrail=5):
    '''參考《基于電動(dòng)汽車的帶時(shí)間窗的路徑優(yōu)化問題研究》中給出的交叉操作,生成一個(gè)子代'''
    # 在ind1中隨機(jī)選擇一段子路徑subroute1嘹锁,將其前置
    routes1 = decodeInd(ind1) # 將ind1解碼成路徑
    numSubroute1 = len(routes1) # 子路徑數(shù)量
    subroute1 = routes1[np.random.randint(0, numSubroute1)]
    # 將subroute1中沒有出現(xiàn)的顧客按照其在ind2中的順序排列成一個(gè)序列
    unvisited = set(ind1) - set(subroute1) # 在subroute1中沒有出現(xiàn)訪問的顧客
    unvisitedPerm = [digit for digit in ind2 if digit in unvisited] # 按照在ind2中的順序排列
    # 多次重復(fù)隨機(jī)打斷葫录,選取適應(yīng)度最好的個(gè)體
    bestRoute = None # 容器
    bestFit = np.inf
    for _ in range(nTrail):
        # 將該序列隨機(jī)打斷為numSubroute1-1條子路徑
        breakPos = [0]+random.sample(range(1,len(unvisitedPerm)),numSubroute1-2) # 產(chǎn)生numSubroute1-2個(gè)斷點(diǎn)
        breakPos.sort()
        breakSubroute = []
        for i,j in zip(breakPos[0::], breakPos[1::]):
            breakSubroute.append([0]+unvisitedPerm[i:j]+[0])
        breakSubroute.append([0]+unvisitedPerm[j:]+[0])
        # 更新適應(yīng)度最佳的打斷方式
        # 將先前取出的subroute1添加入打斷結(jié)果,得到完整的配送方案
        breakSubroute.append(subroute1)
        # 評(píng)價(jià)生成的子路徑
        routesFit = calRouteLen(breakSubroute) + loadPenalty(breakSubroute)
        if routesFit < bestFit:
            bestRoute = breakSubroute
            bestFit = routesFit
    # 將得到的適應(yīng)度最佳路徑bestRoute合并為一個(gè)染色體
    child = []
    for eachRoute in bestRoute:
        child += eachRoute[:-1]
    return child+[0]

def crossover(ind1, ind2):
    '''交叉操作'''
    ind1[:], ind2[:] = genChild(ind1, ind2), genChild(ind2, ind1)
    return ind1, ind2

#-----------------------------------
## 突變操作
def opt(route,dataDict=dataDict, k=2):
    # 用2-opt算法優(yōu)化路徑
    # 輸入:
    # route -- sequence领猾,記錄路徑
    # 輸出: 優(yōu)化后的路徑optimizedRoute及其路徑長(zhǎng)度
    nCities = len(route) # 城市數(shù)
    optimizedRoute = route # 最優(yōu)路徑
    minDistance = calRouteLen([route]) # 最優(yōu)路徑長(zhǎng)度
    for i in range(1,nCities-2):
        for j in range(i+k, nCities):
            if j-i == 1:
                continue
            reversedRoute = route[:i]+route[i:j][::-1]+route[j:]# 翻轉(zhuǎn)后的路徑
            reversedRouteDist = calRouteLen([reversedRoute])
            # 如果翻轉(zhuǎn)后路徑更優(yōu)米同,則更新最優(yōu)解
            if  reversedRouteDist < minDistance:
                minDistance = reversedRouteDist
                optimizedRoute = reversedRoute
    return optimizedRoute

def mutate(ind):
    '''用2-opt算法對(duì)各條子路徑進(jìn)行局部?jī)?yōu)化'''
    routes = decodeInd(ind)
    optimizedAssembly = []
    for eachRoute in routes:
        optimizedRoute = opt(eachRoute)
        optimizedAssembly.append(optimizedRoute)
    # 將路徑重新組裝為染色體
    child = []
    for eachRoute in optimizedAssembly:
        child += eachRoute[:-1]
    ind[:] = child+[0]
    return ind,

#-----------------------------------
## 注冊(cè)遺傳算法操作
toolbox = base.Toolbox()
toolbox.register('individual', tools.initIterate, creator.Individual, genInd)
toolbox.register('population', tools.initRepeat, list, toolbox.individual)
toolbox.register('evaluate', evaluate)
toolbox.register('select', tools.selTournament, tournsize=2)
toolbox.register('mate', crossover)
toolbox.register('mutate', mutate)

## 生成初始族群
toolbox.popSize = 100
pop = toolbox.population(toolbox.popSize)

## 記錄迭代數(shù)據(jù)
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)

## 遺傳算法參數(shù)
toolbox.ngen = 400
toolbox.cxpb = 0.8
toolbox.mutpb = 0.1

## 遺傳算法主程序
## 遺傳算法主程序
pop,logbook=algorithms.eaMuPlusLambda(pop, toolbox, mu=toolbox.popSize, 
                                      lambda_=toolbox.popSize,cxpb=toolbox.cxpb, mutpb=toolbox.mutpb,
                   ngen=toolbox.ngen ,stats=stats, halloffame=hallOfFame, verbose=True)

輸出計(jì)算結(jié)果:

from pprint import pprint

def calLoad(routes):
    loads = []
    for eachRoute in routes:
        routeLoad = np.sum([dataDict['Demand'][i] for i in eachRoute])
        loads.append(routeLoad)
    return loads

bestInd = hallOfFame.items[0]
distributionPlan = decodeInd(bestInd)
bestFit = bestInd.fitness.values
print('最佳運(yùn)輸計(jì)劃為:')
pprint(distributionPlan)
print('最短運(yùn)輸距離為:')
print(bestFit)
print('各輛車上負(fù)載為:')
print(calLoad(distributionPlan))

# 畫出迭代圖
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')

# 計(jì)算結(jié)果
#最佳運(yùn)輸計(jì)劃為:
#[[0, 9, 12, 19, 22, 24, 25, 17, 0],
# [0, 6, 4, 3, 2, 1, 5, 0],
# [0, 7, 0],
# [0, 8, 10, 11, 13, 14, 16, 18, 23, 26, 30, 29, 28, 27, 21, 20, 15, 0]]
#最短運(yùn)輸距離為:
#(136.93713103610511,)
#各輛車上負(fù)載為:
#[200, 290, 20, 391]

迭代過程如下圖所示:

VRP_iterations

總共使用了4輛車,各自的行駛路徑如下:

VRP_routes
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末摔竿,一起剝皮案震驚了整個(gè)濱河市面粮,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌继低,老刑警劉巖熬苍,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異袁翁,居然都是意外死亡柴底,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門粱胜,熙熙樓的掌柜王于貴愁眉苦臉地迎上來柄驻,“玉大人,你說我怎么就攤上這事年柠≡浼撸” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵冗恨,是天一觀的道長(zhǎng)答憔。 經(jīng)常有香客問我,道長(zhǎng)掀抹,這世上最難降的妖魔是什么虐拓? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮傲武,結(jié)果婚禮上蓉驹,老公的妹妹穿的比我還像新娘城榛。我一直安慰自己,他們只是感情好态兴,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布狠持。 她就那樣靜靜地躺著,像睡著了一般瞻润。 火紅的嫁衣襯著肌膚如雪喘垂。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天绍撞,我揣著相機(jī)與錄音正勒,去河邊找鬼。 笑死傻铣,一個(gè)胖子當(dāng)著我的面吹牛章贞,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播非洲,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼鸭限,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了怪蔑?” 一聲冷哼從身側(cè)響起里覆,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤丧荐,失蹤者是張志新(化名)和其女友劉穎缆瓣,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體虹统,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡弓坞,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了车荔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片渡冻。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖忧便,靈堂內(nèi)的尸體忽然破棺而出族吻,到底是詐尸還是另有隱情,我是刑警寧澤珠增,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布超歌,位于F島的核電站,受9級(jí)特大地震影響蒂教,放射性物質(zhì)發(fā)生泄漏巍举。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一凝垛、第九天 我趴在偏房一處隱蔽的房頂上張望懊悯。 院中可真熱鬧蜓谋,春花似錦、人聲如沸炭分。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)捧毛。三九已至覆旭,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間岖妄,已是汗流浹背型将。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留荐虐,地道東北人七兜。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像福扬,于是被迫代替她去往敵國(guó)和親腕铸。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 問題描述 車輛配送模型(Vehicle routing problem)是指從配送中心用車輛把物資配送給顧客時(shí)铛碑,規(guī)...
    ChaoesLuol閱讀 22,182評(píng)論 24 20
  • 摘要:隨著中國(guó)物流運(yùn)輸行業(yè)的蓬勃發(fā)展, 物流成本已經(jīng)占據(jù)了18%的國(guó)民生產(chǎn)總值撇吞, 其中俗冻, 車輛運(yùn)輸作為配送成本的核...
    暖夏未眠丶閱讀 659評(píng)論 0 1
  • 打開一個(gè)前同事的朋友圈。 沒有什么理由牍颈,再也看不見迄薄。 沒關(guān)系。 在群里依舊熱鬧地說話煮岁,只是隱藏了自己的生活軌跡讥蔽。 ...
    果墨一閱讀 90評(píng)論 0 0
  • 我的高中,我的刻骨銘心 過了很久很久以后画机,我才明白那樣子的刻骨銘心冶伞,原來叫做喜歡。 高二的開學(xué)換位置的時(shí)候我們有了...
    豆腐逗你玩閱讀 150評(píng)論 0 0
  • 每次寫東西色罚,第一句總是最難的碰缔,不知道怎么表達(dá)〈粱ぃ總是要用一些廢話進(jìn)行過渡金抡。隨著年齡的增長(zhǎng)瀑焦,聽到、看見甚至是經(jīng)歷一些自...
    青五閱讀 581評(píng)論 0 0