問(wèn)題描述
車輛配送模型(Vehicle routing problem)是指從配送中心用車輛把物資配送給顧客時(shí),規(guī)劃調(diào)用哪些車輛矾屯,按照何種順序配送貨物的問(wèn)題少梁。該問(wèn)題通常假定配送中心與顧客的地理位置之間距離擎值、配送用車輛的載重量和可行駛距離(時(shí)間)均為已知骑素,以高效配送和減少成本為目標(biāo)進(jìn)行決策優(yōu)化。
帶時(shí)間窗的車輛配送模型(Vehicle routing problem with time window冀墨,VRPTW)是在VRP基礎(chǔ)上額外附加配送時(shí)間約束條件產(chǎn)生的一個(gè)變種闸衫。在這類問(wèn)題中,給定配送車輛須到達(dá)顧客點(diǎn)的最早時(shí)間和最遲時(shí)間诽嘉,當(dāng)車輛從一個(gè)或者多個(gè)配送中心向帶時(shí)間窗約束的多個(gè)顧客點(diǎn)配送物資的時(shí)候蔚出,求使費(fèi)用最低的車輛調(diào)度方案。
這個(gè)問(wèn)題基于以下假設(shè):
- 所有距離采用歐幾里得表示
- 每個(gè)顧客只接受一輛車的一次配送
- 從配送中心出發(fā)的車輛必須返回配送中心
- 每輛車載重量相同
- 一條路徑上顧客的總需求量不能超過(guò)車輛的最大載重量
- 每個(gè)顧客分別在各自時(shí)間窗內(nèi)接受服務(wù)
定義為需要服務(wù)的兩個(gè)顧客編號(hào)虫腋,為配送中心的車輛編號(hào)骄酗,為顧客和倉(cāng)庫(kù)的集合。
參數(shù):
: 從顧客到顧客的行駛距離
:從顧客到顧客的行駛時(shí)間
:分配給顧客的服務(wù)時(shí)間
:顧客的需求量
:車輛的最大載重量
:允許到達(dá)顧客處的最早時(shí)間
:允許到達(dá)顧客處的最晚時(shí)間
決策變量:
:車輛到達(dá)顧客處的時(shí)間
:當(dāng)車輛被分配從顧客運(yùn)行到顧客時(shí)悦冀,取1趋翻;否則取0
在給定了參數(shù)和定義了決策變量之后,VRPTW問(wèn)題可以用數(shù)學(xué)模型表示為:
VRPTW問(wèn)題示例
示例為1個(gè)配送中心盒蟆,位于坐標(biāo)踏烙,30個(gè)顧客的問(wèn)題,為簡(jiǎn)化問(wèn)題历等,設(shè)配送中心只提供一種型號(hào)的卡車讨惩,卡車的最大載重為400,平均行駛速度為30單位距離/單位時(shí)間寒屯,各顧客的位置荐捻、需求量和時(shí)間窗約束如下表所示:
j | j | ||||||||
---|---|---|---|---|---|---|---|---|---|
1 | (3,13) | 50 | 3 | 4 | 16 | (23,22) | 51 | 8 | 9 |
2 | (3,17) | 50 | 4 | 5 | 17 | (24,11) | 20 | 4 | 6 |
3 | (6,18) | 60 | 5 | 7 | 18 | (27,21) | 20 | 7 | 11 |
4 | (8,17) | 30 | 2 | 9 | 19 | (26,6) | 20 | 10 | 15 |
5 | (10,14) | 90 | 10 | 11 | 20 | (26,9) | 30 | 13 | 14 |
6 | (14,13) | 10 | 12 | 13 | 21 | (27,2) | 30 | 17 | 18 |
7 | (15,11) | 20 | 11 | 13 | 22 | (27,4) | 30 | 16 | 18 |
8 | (15,15) | 10 | 1 | 5 | 23 | (27,17) | 10 | 10 | 12 |
9 | (17,11) | 30 | 11 | 15 | 24 | (28,7) | 60 | 13 | 14 |
10 | (17,16) | 20 | 8 | 9 | 25 | (29,14) | 30 | 5 | 8 |
11 | (18,19) | 30 | 10 | 12 | 26 | (29,18) | 20 | 6 | 8 |
12 | (19,9) | 10 | 15 | 16 | 27 | (30,1) | 30 | 7 | 9 |
13 | (19,21) | 10 | 18 | 20 | 28 | (30,8) | 40 | 3 | 6 |
14 | (21,22) | 10 | 1 | 7 | 29 | (30,15) | 20 | 4 | 5 |
15 | (23,9) | 40 | 6 | 9 | 30 | (30,17) | 20 | 6 | 9 |
遺傳算法求解VRPTW問(wèn)題
個(gè)體編碼
遺傳算法求解VRPTW的文獻(xiàn)中有多種編碼方式,這里對(duì)于個(gè)體采用自然數(shù)編碼寡夹,代表配送中心处面,代表顧客;不同車輛的配送路線之間用0分隔(即每輛車都從倉(cāng)庫(kù)出發(fā))菩掏;對(duì)于有個(gè)顧客鸳君,輛車的VRP問(wèn)題來(lái)說(shuō),染色體長(zhǎng)度為患蹂。
例如配送中心有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
懲罰
在交叉和突變產(chǎn)生的子代中,可能會(huì)有兩種違反約束的形式:
- 車輛超載
- 不能在時(shí)間窗口約束給出的最晚時(shí)間點(diǎn)內(nèi)到達(dá)指定顧客處
對(duì)這兩種違反約束的情況采用靜態(tài)懲罰传于,考慮到時(shí)間窗口更容易被違反囱挑,對(duì)其施加較大的懲罰因子。對(duì)這兩種約束違反的懲罰因子分別設(shè)置為10和500沼溜。
交叉
這里參考《基于電動(dòng)汽車的帶時(shí)間窗的路徑優(yōu)化問(wèn)題研究》中給出的交叉操作平挑,細(xì)節(jié)可以參考上一篇博文
突變
對(duì)選中的個(gè)體中各條子路線用2-opt算法優(yōu)化
選擇
- 育種選擇:binary錦標(biāo)賽選擇
- 環(huán)境選擇:采用精英保留策略,合并子代和父代后系草,選擇數(shù)量等同于族群規(guī)模的個(gè)體
代碼示例
完整代碼如下:
## 環(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
#-----------------------------------
## 問(wèn)題定義
creator.create('FitnessMin', base.Fitness, weights=(-1.0,)) # 最小化問(wèn)題
# 給個(gè)體一個(gè)routes屬性用來(lái)記錄其表示的路線
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]
# 將配送中心的服務(wù)時(shí)間設(shè)置為0
dataDict['Timewindow'] = [(0,0), (3,4), (4,5), (5,7), (2,9), (10,11),
(12,13), (11,13), (1,5), (11,15), (8,9),
(10,12), (15,16), (18,20), (1,7), (6,9),
(8,9), (4,6), (7,11), (10,15), (13,14),
(17,18), (16,18), (10,12), (13,14), (5,8),
(6,8), (7,9), (3,6), (4,5), (6,9)
]
dataDict['MaxLoad'] = 400
dataDict['ServiceTime'] = 1
dataDict['Velocity'] = 30 # 車輛的平均行駛速度
def genInd(dataDict = dataDict):
'''生成個(gè)體赏枚, 對(duì)我們的問(wèn)題來(lái)說(shuō),困難之處在于車輛數(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 calcRouteServiceTime(route, dataDict = dataDict):
'''輔助函數(shù),根據(jù)給定路徑蚓曼,計(jì)算到達(dá)該路徑上各顧客的時(shí)間'''
# 初始化serviceTime數(shù)組亲澡,其長(zhǎng)度應(yīng)該比eachRoute小2
serviceTime = [0] * (len(route) - 2)
# 從倉(cāng)庫(kù)到第一個(gè)客戶時(shí)不需要服務(wù)時(shí)間
arrivalTime = calDist(dataDict['NodeCoor'][0], dataDict['NodeCoor'][route[1]])/dataDict['Velocity']
arrivalTime = max(arrivalTime, dataDict['Timewindow'][route[1]][0])
serviceTime[0] = arrivalTime
arrivalTime += dataDict['ServiceTime'] # 在出發(fā)前往下個(gè)節(jié)點(diǎn)前完成服務(wù)
for i in range(1, len(route)-2):
# 計(jì)算從路徑上當(dāng)前節(jié)點(diǎn)[i]到下一個(gè)節(jié)點(diǎn)[i+1]的花費(fèi)的時(shí)間
arrivalTime += calDist(dataDict['NodeCoor'][route[i]], dataDict['NodeCoor'][route[i+1]])/dataDict['Velocity']
arrivalTime = max(arrivalTime, dataDict['Timewindow'][route[i+1]][0])
serviceTime[i] = arrivalTime
arrivalTime += dataDict['ServiceTime'] # 在出發(fā)前往下個(gè)節(jié)點(diǎn)前完成服務(wù)
return serviceTime
def timeTable(distributionPlan, dataDict = dataDict):
'''輔助函數(shù),依照給定配送計(jì)劃纫版,返回每個(gè)顧客受到服務(wù)的時(shí)間'''
# 對(duì)于每輛車的配送路線床绪,第i個(gè)客戶受到服務(wù)的時(shí)間serviceTime[i]是min(TimeWindow[i][0], arrivalTime[i])
# arrivalTime[i] = serviceTime[i-1] + 服務(wù)時(shí)間 + distance(i,j)/averageVelocity
timeArrangement = [] #容器,用于存儲(chǔ)每個(gè)顧客受到服務(wù)的時(shí)間
for eachRoute in distributionPlan:
serviceTime = calcRouteServiceTime(eachRoute)
timeArrangement.append(serviceTime)
# 將數(shù)組重新組織為與基因編碼一致的排列方式
realignedTimeArrangement = [0]
for routeTime in timeArrangement:
realignedTimeArrangement = realignedTimeArrangement + routeTime + [0]
return realignedTimeArrangement
def timePenalty(ind, routes):
'''輔助函數(shù)其弊,對(duì)不能按服務(wù)時(shí)間到達(dá)顧客的情況進(jìn)行懲罰'''
timeArrangement = timeTable(routes) # 對(duì)給定路線癞己,計(jì)算到達(dá)每個(gè)客戶的時(shí)間
# 索引給定的最遲到達(dá)時(shí)間
desiredTime = [dataDict['Timewindow'][ind[i]][1] for i in range(len(ind))]
# 如果最遲到達(dá)時(shí)間大于實(shí)際到達(dá)客戶的時(shí)間,則延遲為0梭伐,否則延遲設(shè)為實(shí)際到達(dá)時(shí)間與最遲到達(dá)時(shí)間之差
timeDelay = [max(timeArrangement[i]-desiredTime[i],0) for i in range(len(ind))]
return np.sum(timeDelay)/len(timeDelay)
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, c1=10.0, c2=500.0):
'''評(píng)價(jià)函數(shù)糊识,返回解碼后路徑的總長(zhǎng)度绩社,c1, c2分別為車輛超載與不能服從給定時(shí)間窗口的懲罰系數(shù)'''
routes = decodeInd(ind) # 將個(gè)體解碼為路線
totalDistance = calRouteLen(routes)
return (totalDistance + c1*loadPenalty(routes) + c2*timePenalty(ind,routes)),
#-----------------------------------
## 交叉操作
def genChild(ind1, ind2, nTrail=5):
'''參考《基于電動(dòng)汽車的帶時(shí)間窗的路徑優(yōu)化問(wèn)題研究》中給出的交叉操作摔蓝,生成一個(gè)子代'''
# 在ind1中隨機(jī)選擇一段子路徑subroute1,將其前置
routes1 = decodeInd(ind1) # 將ind1解碼成路徑
numSubroute1 = len(routes1) # 子路徑數(shù)量
subroute1 = routes1[np.random.randint(0, numSubroute1)]
# 將subroute1中沒(méi)有出現(xiàn)的顧客按照其在ind2中的順序排列成一個(gè)序列
unvisited = set(ind1) - set(subroute1) # 在subroute1中沒(méi)有出現(xiàn)訪問(wè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, c1=1.0, c2=500.0):
# 用2-opt算法優(yōu)化路徑
# 輸入:
# route -- sequence,記錄路徑
# k -- k-opt朴沿,這里用2opt
# c1, c2 -- 尋求最短路徑長(zhǎng)度和滿足時(shí)間窗口的相對(duì)重要程度
# 輸出: 優(yōu)化后的路徑optimizedRoute及其路徑長(zhǎng)度
nCities = len(route) # 城市數(shù)
optimizedRoute = route # 最優(yōu)路徑
desiredTime = [dataDict['Timewindow'][route[i]][1] for i in range(len(route))]
serviceTime = calcRouteServiceTime(route)
timewindowCost = [max(serviceTime[i]-desiredTime[1:-1][i],0) for i in range(len(serviceTime))]
timewindowCost = np.sum(timewindowCost)/len(timewindowCost)
minCost = c1*calRouteLen([route]) + c2*timewindowCost # 最優(yōu)路徑代價(jià)
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)后的路徑
# 代價(jià)函數(shù)中需要同時(shí)兼顧到達(dá)時(shí)間和路徑長(zhǎng)度
desiredTime = [dataDict['Timewindow'][reversedRoute[i]][1] for i in range(len(reversedRoute))]
serviceTime = calcRouteServiceTime(reversedRoute)
timewindowCost = [max(serviceTime[i]-desiredTime[1:-1][i],0) for i in range(len(serviceTime))]
timewindowCost = np.sum(timewindowCost)/len(timewindowCost)
reversedRouteCost = c1*calRouteLen([reversedRoute]) + c2*timewindowCost
# 如果翻轉(zhuǎn)后路徑更優(yōu)猜谚,則更新最優(yōu)解
if reversedRouteCost < minCost:
minCost = reversedRouteCost
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)
輸出結(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(evaluate(bestInd,c1=0,c2=0))
print('各輛車上負(fù)載為:')
print(calLoad(distributionPlan))
timeArrangement = timeTable(distributionPlan) # 對(duì)給定路線,計(jì)算到達(dá)每個(gè)客戶的時(shí)間
# 索引給定的最遲到達(dá)時(shí)間
desiredTime = [dataDict['Timewindow'][bestInd[i]][1] for i in range(len(bestInd))]
# 如果最遲到達(dá)時(shí)間大于實(shí)際到達(dá)客戶的時(shí)間赌渣,則延遲為0魏铅,否則延遲設(shè)為實(shí)際到達(dá)時(shí)間與最遲到達(dá)時(shí)間之差
timeDelay = [max(timeArrangement[i]-desiredTime[i],0) for i in range(len(bestInd))]
print('到達(dá)各客戶的延遲為:')
print(timeDelay)
# 畫出迭代圖
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, 1, 2, 3, 4, 6, 0],
# [0, 9, 0],
# [0, 14, 29, 17, 30, 26, 18, 23, 21, 0],
# [0, 8, 25, 15, 16, 0],
# [0, 10, 11, 24, 12, 0],
# [0, 5, 7, 0],
# [0, 28, 27, 20, 19, 22, 13, 0]]
#總運(yùn)輸距離為:
#(278.62210617851554,)
#各輛車上負(fù)載為:
#[200, 30, 150, 131, 120, 110, 160]
#到達(dá)各客戶的延遲為:
#[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
迭代過(guò)程可視化:
配送路線可視化: