基于python語言,實作自适應大鄰域搜尋算法(ALNS)對多車場車輛路徑規劃問題(MDCVRP)進行求解。
目錄
- 1. 适用場景
- 2. 求解效果
- 3. 代碼分析
- 4. 資料格式
- 5. 分步實作
- 6. 完整代碼
1. 适用場景
- 求解MDCVRP 問題
- 車輛類型單一
- 車輛容量不小于需求節點最大需求
- 多車輛基地
- 各車場車輛總數滿足實際需求
2. 求解效果
(1)收斂曲線
(2)車輛路徑
3. 代碼分析
車倆路徑規劃問題按照車場數量可分為單一車場路徑規劃問題和多車場路徑規劃問題。針對單一車場條件下僅考慮車輛容量限制的路徑規劃問題前邊文章中已實作ALNS算法求解,本文采用ALNS算法對多車場條件下僅考慮車輛容量限制的路徑規劃問題進行求解。為了保持代碼的延續性以及求解思路的一緻性,這裡對上述ALNS算法代碼進行如下主要修正實作MDCVRP問題的求解。
- Model資料結構類中采用demand_dict屬性存儲需求節點集合,depot_dict屬性儲存車場節點集合,demand_id_list屬性儲存需求節點id集合,distance_matrix屬性儲存任意節點間的歐式距離;
- Node資料結構類中去掉node_seq屬性,增加depot_capacity屬性記錄車場的車隊限制
- splitRoutes函數中分割車輛路徑時配置設定最近車場作為其車輛提供基地(滿足車場車隊數量限制)
4. 資料格式
以csv檔案儲存資料,其中demand.csv檔案記錄需求節點資料,共包含需求節點id,需求節點橫坐标,需求節點縱坐标,需求量;depot.csv檔案記錄車場節點資料,共包含車場id,車場橫坐标,車場縱坐标,車隊數量。需要注意的是:需求節點id應為整數,車場節點id任意,但不可與需求節點id重複。 可參考github首頁相關檔案。
5. 分步實作
(1)資料結構
為便于資料處理,定義Sol()類,Node()類,Model()類,其屬性如下表:
- Sol()類,表示一個可行解
屬性 | 描述 |
---|---|
node_id_list | 需求節點id有序排列集合,對應TSP的解 |
obj | 優化目标值 |
routes | 車輛路徑集合,對應MDCVRP的解 |
- Node()類,表示一個網絡節點
屬性 | 描述 |
---|---|
id | 實體節點id |
x_coord | 實體節點x坐标 |
y_coord | 實體節點y坐标 |
demand | 實體節點需求 |
depot_capacity | 車輛基地車隊規模 |
- Model()類,存儲算法參數
屬性 | 描述 |
---|---|
best_sol | 全局最優解,值類型為Sol() |
demand_dict | 需求節點集合(字典),值類型為Node() |
depot_dict | 車場節點集合(字典),值類型為Node() |
demand_id_list | 需求節點id集合 |
distance_matrix | 節點距離矩陣 |
opt_type | 優化目标類型,0:最小車輛數,1:最小行駛距離 |
vehicle_cap | 車輛容量 |
rand_d_max | 随機破壞程度上限 |
rand_d_min | 随機破壞程度下限 |
worst_d_max | 最壞破壞程度上限 |
worst_d_min | 最壞破壞程度下限 |
regret_n | 次優位置個數 |
r1 | 算子獎勵1 |
r2 | 算子獎勵2 |
r3 | 算子獎勵3 |
rho | 算子權重衰減系數 |
d_weight | 破壞算子權重 |
d_select | 破壞算子被選中次數/每輪 |
d_score | 破壞算子被獎勵得分/每輪 |
d_history_select | 破壞算子曆史共計被選中次數 |
d_history_score | 破壞算子曆史共計被獎勵得分 |
r_weight | 修複算子權重 |
r_select | 修複算子被選中次數/每輪 |
r_score | 修複算子被獎勵得分/每輪 |
r_history_select | 修複算子曆史共計被選中次數 |
r_history_score | 修複算子曆史共計被獎勵得分 |
(2)檔案讀取
def readCsvFile(demand_file, depot_file, model):
with open(demand_file,'r') as f:
demand_reader=csv.DictReader(f)
for row in demand_reader:
node = Node()
node.id = int(row['id'])
node.x_coord = float(row['x_coord'])
node.y_coord = float(row['y_coord'])
node.demand = float(row['demand'])
model.demand_dict[node.id] = node
model.demand_id_list.append(node.id)
with open(depot_file,'r') as f:
depot_reader=csv.DictReader(f)
for row in depot_reader:
node = Node()
node.id = row['id']
node.x_coord=float(row['x_coord'])
node.y_coord=float(row['y_coord'])
node.depot_capacity=float(row['capacity'])
model.depot_dict[node.id] = node
(3)計算距離矩陣
def calDistance(model):
for i in range(len(model.demand_id_list)):
from_node_id = model.demand_id_list[i]
for j in range(i+1,len(model.demand_id_list)):
to_node_id=model.demand_id_list[j]
dist=math.sqrt( (model.demand_dict[from_node_id].x_coord-model.demand_dict[to_node_id].x_coord)**2
+(model.demand_dict[from_node_id].y_coord-model.demand_dict[to_node_id].y_coord)**2)
model.distance_matrix[from_node_id,to_node_id]=dist
model.distance_matrix[to_node_id,from_node_id]=dist
for _,depot in model.depot_dict.items():
dist = math.sqrt((model.demand_dict[from_node_id].x_coord - depot.x_coord) ** 2
+ (model.demand_dict[from_node_id].y_coord -depot.y_coord)**2)
model.distance_matrix[from_node_id, depot.id] = dist
model.distance_matrix[depot.id, from_node_id] = dist
(4)目标函數計算
适應度計算依賴" splitRoutes “函數對TSP可行解分割得到車輛行駛路線和所需車輛數,在得到各車輛行駛路線後調用” selectDepot “函數,在滿足車場車隊規模條件下配置設定最近車場,” calDistance "函數計算行駛距離。
def selectDepot(route,depot_dict,model):
min_in_out_distance=float('inf')
index=None
for _,depot in depot_dict.items():
if depot.depot_capacity>0:
in_out_distance=model.distance_matrix[depot.id,route[0]]+model.distance_matrix[route[-1],depot.id]
if in_out_distance<min_in_out_distance:
index=depot.id
min_in_out_distance=in_out_distance
if index is None:
print("there is no vehicle to dispatch")
route.insert(0,index)
route.append(index)
depot_dict[index].depot_capacity=depot_dict[index].depot_capacity-1
return route,depot_dict
def splitRoutes(node_id_list,model):
num_vehicle = 0
vehicle_routes = []
route = []
remained_cap = model.vehicle_cap
depot_dict=copy.deepcopy(model.depot_dict)
for node_id in node_id_list:
if remained_cap - model.demand_dict[node_id].demand >= 0:
route.append(node_id)
remained_cap = remained_cap - model.demand_dict[node_id].demand
else:
route,depot_dict=selectDepot(route,depot_dict,model)
vehicle_routes.append(route)
route = [node_id]
num_vehicle = num_vehicle + 1
remained_cap =model.vehicle_cap - model.demand_dict[node_id].demand
route, depot_dict = selectDepot(route, depot_dict, model)
vehicle_routes.append(route)
return num_vehicle,vehicle_routes
def calRouteDistance(route,model):
distance=0
for i in range(len(route)-1):
from_node=route[i]
to_node=route[i+1]
distance +=model.distance_matrix[from_node,to_node]
return distance
def calObj(node_id_list,model):
num_vehicle, vehicle_routes = splitRoutes(node_id_list, model)
if model.opt_type==0:
return num_vehicle,vehicle_routes
else:
distance = 0
for route in vehicle_routes:
distance += calRouteDistance(route, model)
return distance,vehicle_routes
(5)初始解
def genInitialSol(node_id_list):
node_id_list=copy.deepcopy(node_id_list)
random.seed(0)
random.shuffle(node_id_list)
return node_id_list
(6)定義destroy算子(破壞算子)
這裡實作兩種destory:
- random destroy : 随機從目前解中移除一定比例的需求節點
- worst destroy:從目前解中移除一定比例引起目标函數增幅較大的需求節點
def createRandomDestory(model):
d=random.uniform(model.rand_d_min,model.rand_d_max)
reomve_list=random.sample(range(len(model.demand_id_list)),int(d*len(model.demand_id_list)))
return reomve_list
def createWorseDestory(model,sol):
deta_f=[]
for node_id in sol.node_id_list:
nodes_id_=copy.deepcopy(sol.node_id_list)
nodes_id_.remove(node_id)
obj,vehicle_routes=calObj(nodes_id_,model)
deta_f.append(sol.obj-obj)
sorted_id = sorted(range(len(deta_f)), key=lambda k: deta_f[k], reverse=True)
d=random.randint(model.worst_d_min,model.worst_d_max)
remove_list=sorted_id[:d]
return remove_list
(7)定義repair算子(修複算子)
def createRandomRepair(remove_list,model,sol):
unassigned_nodes_id=[]
assigned_nodes_id = []
# remove node from current solution
for i in range(len(model.demand_id_list)):
if i in remove_list:
unassigned_nodes_id.append(sol.node_id_list[i])
else:
assigned_nodes_id.append(sol.node_id_list[i])
# insert
for node_id in unassigned_nodes_id:
index=random.randint(0,len(assigned_nodes_id)-1)
assigned_nodes_id.insert(index,node_id)
new_sol=Sol()
new_sol.node_id_list=copy.deepcopy(assigned_nodes_id)
new_sol.obj,new_sol.routes=calObj(assigned_nodes_id,model)
return new_sol
def findGreedyInsert(unassigned_nodes_id,assigned_nodes_id,model):
best_insert_node_id=None
best_insert_index = None
best_insert_cost = float('inf')
assigned_nodes_id_obj,_=calObj(assigned_nodes_id,model)
for node_id in unassigned_nodes_id:
for i in range(len(assigned_nodes_id)):
assigned_nodes_id_ = copy.deepcopy(assigned_nodes_id)
assigned_nodes_id_.insert(i, node_id)
obj_, _ = calObj(assigned_nodes_id_, model)
deta_f = obj_ - assigned_nodes_id_obj
if deta_f<best_insert_cost:
best_insert_index=i
best_insert_node_id=node_id
best_insert_cost=deta_f
return best_insert_node_id,best_insert_index
def createGreedyRepair(remove_list,model,sol):
unassigned_nodes_id = []
assigned_nodes_id = []
# remove node from current solution
for i in range(len(model.demand_id_list)):
if i in remove_list:
unassigned_nodes_id.append(sol.node_id_list[i])
else:
assigned_nodes_id.append(sol.node_id_list[i])
#insert
while len(unassigned_nodes_id)>0:
insert_node_id,insert_index=findGreedyInsert(unassigned_nodes_id,assigned_nodes_id,model)
assigned_nodes_id.insert(insert_index,insert_node_id)
unassigned_nodes_id.remove(insert_node_id)
new_sol=Sol()
new_sol.node_id_list=copy.deepcopy(assigned_nodes_id)
new_sol.obj,new_sol.routes=calObj(assigned_nodes_id,model)
return new_sol
def findRegretInsert(unassigned_nodes_id,assigned_nodes_id,model):
opt_insert_node_id = None
opt_insert_index = None
opt_insert_cost = -float('inf')
for node_id in unassigned_nodes_id:
n_insert_cost=np.zeros((len(assigned_nodes_id),3))
for i in range(len(assigned_nodes_id)):
assigned_nodes_id_=copy.deepcopy(assigned_nodes_id)
assigned_nodes_id_.insert(i,node_id)
obj_,_=calObj(assigned_nodes_id_,model)
n_insert_cost[i,0]=node_id
n_insert_cost[i,1]=i
n_insert_cost[i,2]=obj_
n_insert_cost= n_insert_cost[n_insert_cost[:, 2].argsort()]
deta_f=0
for i in range(1,model.regret_n):
deta_f=deta_f+n_insert_cost[i,2]-n_insert_cost[0,2]
if deta_f>opt_insert_cost:
opt_insert_node_id = int(n_insert_cost[0, 0])
opt_insert_index=int(n_insert_cost[0,1])
opt_insert_cost=deta_f
return opt_insert_node_id,opt_insert_index
def createRegretRepair(remove_list,model,sol):
unassigned_nodes_id = []
assigned_nodes_id = []
# remove node from current solution
for i in range(len(model.demand_id_list)):
if i in remove_list:
unassigned_nodes_id.append(sol.node_id_list[i])
else:
assigned_nodes_id.append(sol.node_id_list[i])
# insert
while len(unassigned_nodes_id)>0:
insert_node_id,insert_index=findRegretInsert(unassigned_nodes_id,assigned_nodes_id,model)
assigned_nodes_id.insert(insert_index,insert_node_id)
unassigned_nodes_id.remove(insert_node_id)
new_sol = Sol()
new_sol.node_id_list = copy.deepcopy(assigned_nodes_id)
new_sol.obj, new_sol.routes = calObj(assigned_nodes_id, model)
return new_sol
(8)随機選擇算子
def selectDestoryRepair(model):
d_weight=model.d_weight
d_cumsumprob = (d_weight / sum(d_weight)).cumsum()
d_cumsumprob -= np.random.rand()
destory_id= list(d_cumsumprob > 0).index(True)
r_weight=model.r_weight
r_cumsumprob = (r_weight / sum(r_weight)).cumsum()
r_cumsumprob -= np.random.rand()
repair_id = list(r_cumsumprob > 0).index(True)
return destory_id,repair_id
(9)執行destory算子
def doDestory(destory_id,model,sol):
if destory_id==0:
reomve_list=createRandomDestory(model)
else:
reomve_list=createWorseDestory(model,sol)
return reomve_list
(10)執行repair算子
def doRepair(repair_id,reomve_list,model,sol):
if repair_id==0:
new_sol=createRandomRepair(reomve_list,model,sol)
elif repair_id==1:
new_sol=createGreedyRepair(reomve_list,model,sol)
else:
new_sol=createRegretRepair(reomve_list,model,sol)
return new_sol
(11)重置算子得分
def resetScore(model):
model.d_select = np.zeros(2)
model.d_score = np.zeros(2)
model.r_select = np.zeros(3)
model.r_score = np.zeros(3)
(12)更新算子權重
def updateWeight(model):
for i in range(model.d_weight.shape[0]):
if model.d_select[i]>0:
model.d_weight[i]=model.d_weight[i]*(1-model.rho)+model.rho*model.d_score[i]/model.d_select[i]
else:
model.d_weight[i] = model.d_weight[i] * (1 - model.rho)
for i in range(model.r_weight.shape[0]):
if model.r_select[i]>0:
model.r_weight[i]=model.r_weight[i]*(1-model.rho)+model.rho*model.r_score[i]/model.r_select[i]
else:
model.r_weight[i] = model.r_weight[i] * (1 - model.rho)
model.d_history_select = model.d_history_select + model.d_select
model.d_history_score = model.d_history_score + model.d_score
model.r_history_select = model.r_history_select + model.r_select
model.r_history_score = model.r_history_score + model.r_score
(13)繪制收斂曲線
def plotObj(obj_list):
plt.rcParams['font.sans-serif'] = ['SimHei'] #show chinese
plt.rcParams['axes.unicode_minus'] = False # Show minus sign
plt.plot(np.arange(1,len(obj_list)+1),obj_list)
plt.xlabel('Iterations')
plt.ylabel('Obj Value')
plt.grid()
plt.xlim(1,len(obj_list)+1)
plt.show()
(14)繪制車輛路線
def plotRoutes(model):
for route in model.best_sol.routes:
x_coord=[model.depot_dict[route[0]].x_coord]
y_coord=[model.depot_dict[route[0]].y_coord]
for node_id in route[1:-1]:
x_coord.append(model.demand_dict[node_id].x_coord)
y_coord.append(model.demand_dict[node_id].y_coord)
x_coord.append(model.depot_dict[route[-1]].x_coord)
y_coord.append(model.depot_dict[route[-1]].y_coord)
plt.grid()
if route[0]=='d1':
plt.plot(x_coord,y_coord,marker='o',color='black',linewidth=0.5,markersize=5)
elif route[0]=='d2':
plt.plot(x_coord,y_coord,marker='o',color='orange',linewidth=0.5,markersize=5)
else:
plt.plot(x_coord,y_coord,marker='o',color='b',linewidth=0.5,markersize=5)
plt.xlabel('x_coord')
plt.ylabel('y_coord')
plt.show()
(15)輸出結果
def outPut(model):
work=xlsxwriter.Workbook('result.xlsx')
worksheet=work.add_worksheet()
worksheet.write(0,0,'opt_type')
worksheet.write(1,0,'obj')
if model.opt_type==0:
worksheet.write(0,1,'number of vehicles')
else:
worksheet.write(0, 1, 'drive distance of vehicles')
worksheet.write(1,1,model.best_sol.obj)
for row,route in enumerate(model.best_sol.routes):
worksheet.write(row+2,0,'v'+str(row+1))
r=[str(i)for i in route]
worksheet.write(row+2,1, '-'.join(r))
work.close()
(16)主函數
def run(demand_file,depot_file,rand_d_max,rand_d_min,worst_d_min,worst_d_max,regret_n,r1,r2,r3,rho,phi,epochs,pu,v_cap,opt_type):
"""
:param demand_file: demand file path
:param depot_file: depot file path
:param rand_d_max: max degree of random destruction
:param rand_d_min: min degree of random destruction
:param worst_d_max: max degree of worst destruction
:param worst_d_min: min degree of worst destruction
:param regret_n: n next cheapest insertions
:param r1: score if the new solution is the best one found so far.
:param r2: score if the new solution improves the current solution.
:param r3: score if the new solution does not improve the current solution, but is accepted.
:param rho: reaction factor of action weight
:param phi: the reduction factor of threshold
:param epochs: Iterations
:param pu: the frequency of weight adjustment
:param v_cap: Vehicle capacity
:param opt_type: Optimization type:0:Minimize the number of vehicles,1:Minimize travel distance
:return:
"""
model=Model()
model.rand_d_max=rand_d_max
model.rand_d_min=rand_d_min
model.worst_d_min=worst_d_min
model.worst_d_max=worst_d_max
model.regret_n=regret_n
model.r1=r1
model.r2=r2
model.r3=r3
model.rho=rho
model.vehicle_cap=v_cap
model.opt_type=opt_type
readCsvFile(demand_file,depot_file, model)
calDistance(model)
history_best_obj = []
sol = Sol()
sol.node_id_list = genInitialSol(model.demand_id_list)
sol.obj, sol.routes = calObj(sol.node_id_list, model)
model.best_sol = copy.deepcopy(sol)
history_best_obj.append(sol.obj)
for ep in range(epochs):
T=sol.obj*0.2
resetScore(model)
for k in range(pu):
destory_id,repair_id=selectDestoryRepair(model)
model.d_select[destory_id]+=1
model.r_select[repair_id]+=1
reomve_list=doDestory(destory_id,model,sol)
new_sol=doRepair(repair_id,reomve_list,model,sol)
if new_sol.obj<sol.obj:
sol=copy.deepcopy(new_sol)
if new_sol.obj<model.best_sol.obj:
model.best_sol=copy.deepcopy(new_sol)
model.d_score[destory_id]+=model.r1
model.r_score[repair_id]+=model.r1
else:
model.d_score[destory_id]+=model.r2
model.r_score[repair_id]+=model.r2
elif new_sol.obj-sol.obj<T:
sol=copy.deepcopy(new_sol)
model.d_score[destory_id] += model.r3
model.r_score[repair_id] += model.r3
T=T*phi
print("%s/%s:%s/%s, best obj: %s" % (ep,epochs,k,pu, model.best_sol.obj))
history_best_obj.append(model.best_sol.obj)
updateWeight(model)
plotObj(history_best_obj)
plotRoutes(model)
outPut(model)
print("random destory weight is {:.3f}\tselect is {}\tscore is {:.3f}".format(model.d_weight[0],
model.d_history_select[0],
model.d_history_score[0]))
print("worse destory weight is {:.3f}\tselect is {}\tscore is {:.3f} ".format(model.d_weight[1],
model.d_history_select[1],
model.d_history_score[1]))
print("random repair weight is {:.3f}\tselect is {}\tscore is {:.3f}".format(model.r_weight[0],
model.r_history_select[0],
model.r_history_score[0]))
print("greedy repair weight is {:.3f}\tselect is {}\tscore is {:.3f}".format(model.r_weight[1],
model.r_history_select[1],
model.r_history_score[1]))
print("regret repair weight is {:.3f}\tselect is {}\tscore is {:.3f}".format(model.r_weight[2],
model.r_history_select[2],
model.r_history_score[2]))
6. 完整代碼
代碼和資料檔案可從github首頁免費擷取:
https://github.com/PariseC/Algorithms_for_solving_VRP