天天看點

【進階一】Python實作MDCVRP常見求解算法——自适應大鄰域搜尋算法(ALNS)1. 适用場景2. 求解效果3. 代碼分析4. 資料格式5. 分步實作6. 完整代碼

基于python語言,實作自适應大鄰域搜尋算法(ALNS)對多車場車輛路徑規劃問題(MDCVRP)進行求解。

目錄

  • 1. 适用場景
  • 2. 求解效果
  • 3. 代碼分析
  • 4. 資料格式
  • 5. 分步實作
  • 6. 完整代碼

1. 适用場景

  • 求解MDCVRP 問題
  • 車輛類型單一
  • 車輛容量不小于需求節點最大需求
  • 多車輛基地
  • 各車場車輛總數滿足實際需求

2. 求解效果

(1)收斂曲線

【進階一】Python實作MDCVRP常見求解算法——自适應大鄰域搜尋算法(ALNS)1. 适用場景2. 求解效果3. 代碼分析4. 資料格式5. 分步實作6. 完整代碼

(2)車輛路徑

【進階一】Python實作MDCVRP常見求解算法——自适應大鄰域搜尋算法(ALNS)1. 适用場景2. 求解效果3. 代碼分析4. 資料格式5. 分步實作6. 完整代碼

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