利用or-tools来求解路径规划问题(VRP)

4 篇文章 4 订阅
4 篇文章 1 订阅

在之前的博客中我们介绍了用or-tools来求解旅行商问题(TSP),一般来说VRP问题的复杂度要大于TSP, 也可以认为TSP是一个简化版的VRP,今天我们先讨论一下更为复杂一点的的VRP问题.

车辆路径规划问题(VRP)

在车辆路线规划问题 (VRP) 中,我们的目标是让多辆车访问多个地点,因此要为每辆车找到访问各个地点的最佳路线(当只有一辆车时,它简化为旅行商TSP问题),这种应用场景主要集中在交通运输、物流、快递等行业中。VRP又可以拓展出2种新的应用场景:1.CVRP 带容量限制的VRP, 2. VRPTW 带时间窗口的VRP,我们会在后续的博客中介绍CVRP和VRPTW这两种应用。

这里的最佳路线指的是每辆车访问各个不同地点的成本最低,成本最低可以是指访问路线最短,或者是时间最短,在本案例中我们使用最短路线作为最低成本。

VRP 示例

假设有一家物流公司要为某个城市中的若干个客户派送快递。 下图显示了客户所在位置的图表,物流公司位置标记为黑色,客户所在地点标记为蓝色。

这里需要说明一下:

地点0: 公司所在地,也就是所有车辆出发地。

地点1-地点16:客户所在地位置,所以一共有16个需要访问的点。

这些地点其实是位于地图上的地点,为了简单化我们将这些地点放到了一个矩形表格中这样处理更加直观一些便于理解, 然后我们会通过一些第三方的地图接口(百度、高德)去计算每个需要访问的地点它们两两之间的实际距离,并得到一个距离矩阵(distance_matrix)。计算距离矩阵的过程在这里不做说明,如果读者感兴趣可以参考第三方地图接口的文档说明。

定义数据模型


       这里我们定义了一个数据模型,数据模型中包含了各个地点两两之间的距离矩阵(distance_matrix),车辆数量(num_vehicles),出发和返回点的索引(depot).其中:

  • data['distance_matrix']:距离矩阵,是一个17×17的矩阵它表示17个地点两两之间的距离。(我们将出发地也作为一个地点,所有车辆必须从出发地出发,最后必须返回出发地。)
  • data['num_vehicles']:车辆数量,这里我们有4辆车来访问16个地点(每辆车访问各种不同的地点)。
  • data['depot']:出发和返回点的索引,这里定义为0表示从第一个地点出发最后返回第一个地点。
from ortools.constraint_solver import routing_enums_pb2
from ortools.constraint_solver import pywrapcp


def create_data_model():
    """Stores the data for the problem."""
    data = {}
    data['distance_matrix'] = [
        [
            0, 548, 776, 696, 582, 274, 502, 194, 308, 194, 536, 502, 388, 354,
            468, 776, 662
        ],
        [
            548, 0, 684, 308, 194, 502, 730, 354, 696, 742, 1084, 594, 480, 674,
            1016, 868, 1210
        ],
        [
            776, 684, 0, 992, 878, 502, 274, 810, 468, 742, 400, 1278, 1164,
            1130, 788, 1552, 754
        ],
        [
            696, 308, 992, 0, 114, 650, 878, 502, 844, 890, 1232, 514, 628, 822,
            1164, 560, 1358
        ],
        [
            582, 194, 878, 114, 0, 536, 764, 388, 730, 776, 1118, 400, 514, 708,
            1050, 674, 1244
        ],
        [
            274, 502, 502, 650, 536, 0, 228, 308, 194, 240, 582, 776, 662, 628,
            514, 1050, 708
        ],
        [
            502, 730, 274, 878, 764, 228, 0, 536, 194, 468, 354, 1004, 890, 856,
            514, 1278, 480
        ],
        [
            194, 354, 810, 502, 388, 308, 536, 0, 342, 388, 730, 468, 354, 320,
            662, 742, 856
        ],
        [
            308, 696, 468, 844, 730, 194, 194, 342, 0, 274, 388, 810, 696, 662,
            320, 1084, 514
        ],
        [
            194, 742, 742, 890, 776, 240, 468, 388, 274, 0, 342, 536, 422, 388,
            274, 810, 468
        ],
        [
            536, 1084, 400, 1232, 1118, 582, 354, 730, 388, 342, 0, 878, 764,
            730, 388, 1152, 354
        ],
        [
            502, 594, 1278, 514, 400, 776, 1004, 468, 810, 536, 878, 0, 114,
            308, 650, 274, 844
        ],
        [
            388, 480, 1164, 628, 514, 662, 890, 354, 696, 422, 764, 114, 0, 194,
            536, 388, 730
        ],
        [
            354, 674, 1130, 822, 708, 628, 856, 320, 662, 388, 730, 308, 194, 0,
            342, 422, 536
        ],
        [
            468, 1016, 788, 1164, 1050, 514, 514, 662, 320, 274, 388, 650, 536,
            342, 0, 764, 194
        ],
        [
            776, 868, 1552, 560, 674, 1050, 1278, 742, 1084, 810, 1152, 274,
            388, 422, 764, 0, 798
        ],
        [
            662, 1210, 754, 1358, 1244, 708, 480, 856, 514, 468, 354, 844, 730,
            536, 194, 798, 0
        ],
    ]
    data['num_vehicles'] = 4 #汽车数量
    data['depot'] = 0 #出发、返回地索引
    return data

创建路由模型

        和TSP代码类似,以下代码在程序的主要部分创建了索引管理器(manager)和路由模型(routing)。 manager主要根据distance_matrix来管理各个城市的索引,而routing用来计算和存储访问路径。

# Create the routing index manager.
manager = pywrapcp.RoutingIndexManager(len(data['distance_matrix']),
                                           data['num_vehicles'], data['depot'])

# Create Routing Model.
routing = pywrapcp.RoutingModel(manager)

创建距离回调函数

    这里我们定义了一个距离回调函数用来从distance_matrix中返回给定的两个城市之间的距离,接下来我们还要设置路由成本(routing.SetArcCostEvaluatorOfAllVehicles)它将告诉求解器如何计算任意两个地点的路线成本—这里我们的成本指的是任意两个地点之间的距离
 

def distance_callback(from_index, to_index):
    from_node = manager.IndexToNode(from_index)
    to_node = manager.IndexToNode(to_index)
    return data['distance_matrix'][from_node][to_node]

transit_callback_index = routing.RegisterTransitCallback(distance_callback)
routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)

定义距离维度

与TSP不同,在VRP地场景中我们还需要创建一个距离维度,它用来计算每辆车沿其路线行驶的累积距离。然后,您可以设置与沿每条路线的总距离的最大值成比例的成本。路线规划程序使用维度来跟踪车辆路线上累积的数量。

下面地代码使用求解器的AddDimension方法创建距离维度 。

​
dimension_name = 'Distance'
routing.AddDimension(
    transit_callback_index, #callback_index
    0,  # slack_max
    3000,  # capacity
    True,  # sfix_start_cumulative_to_zero
    dimension_name)
distance_dimension = routing.GetDimensionOrDie(dimension_name)
distance_dimension.SetGlobalSpanCostCoefficient(100)

AddDimension方法的参数说明:

  • callback_index:返回两个地点之间距离的回调的索引。索引是求解器对回调的内部引用,由 RegisterTransitCallback 或 RegisterUnitaryTransitCallback 等方法创建。
  • slack_max:slack 的最大值,一个变量,用于表示位置的等待时间。如果问题不涉及等待时间,通常可以将 slack_max 设置为 0。
  • capacity:容量,沿每条路线累积的总数量的最大值。使用容量来创建类似于 CVRP 中的约束。如果我们的问题没有这样的约束,可以将capacity设置为一个足够大的值,以便对路由没有限制—例如,用于定义回调的矩阵或数组的所有条目的总和。
  • fix_start_cumulative_to_zero:布尔值。如果为 true,则数量的累计值从 0 开始。在大多数情况下,应将其设置为 True。但是,对于VRPTW或资源限制问题,由于时间窗口限制,某些车辆可能必须在时间0之后启动,因此您应该针对这些问题将fix_start_cumulative_to_zero设置为False。
  • 维度名称:维度名称的字符串,例如“距离”,您可以使用它来访问程序中其他地方的变量。 

distance_dimension为路由的全局跨度SetGlobalSpanCostCoefficient设置了一个大系数(100),在本例中为路由距离的最大值。这使它成为目标函数中的主要因素,因此我们的优化目标是将最长路线的长度最小化。 

添加打印输出函数

def print_solution(data, manager, routing, solution):
    """Prints solution on console."""
    print(f'Objective: {solution.ObjectiveValue()}')
    max_route_distance = 0
    for vehicle_id in range(data['num_vehicles']):
        index = routing.Start(vehicle_id)
        plan_output = 'Route for vehicle {}:\n'.format(vehicle_id)
        route_distance = 0
        while not routing.IsEnd(index):
            plan_output += ' {} -> '.format(manager.IndexToNode(index))
            previous_index = index
            index = solution.Value(routing.NextVar(index))
            route_distance += routing.GetArcCostForVehicle(
                previous_index, index, vehicle_id)
        plan_output += '{}\n'.format(manager.IndexToNode(index))
        plan_output += 'Distance of the route: {}m\n'.format(route_distance)
        print(plan_output)
        max_route_distance = max(route_distance, max_route_distance)
    print('Maximum of the route distances: {}m'.format(max_route_distance))

设置搜索策略

类似于之前TSP应用中的设置,这里我们也需要设置 first_solution_strategy:PATH_CHEAPEST_ARC

# Setting first solution heuristic.
search_parameters = pywrapcp.DefaultRoutingSearchParameters()
search_parameters.first_solution_strategy = (routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)

求解并输出结果

# Solve the problem.
solution = routing.SolveWithParameters(search_parameters)

# Print solution on console.
if solution:
    print_solution(data, manager, routing, solution)
else:
    print('No solution found !')

 根据上面的输出结果我们在之前的地点图中绘制绿中红、蓝、绿、黄四条路线代表了4辆车的行驶路线,如下图所示:

参考资料 

https://developers.google.com/optimization/routing/vrp

  • 5
    点赞
  • 51
    收藏
    觉得还不错? 一键收藏
  • 5
    评论
以下是使用NSGA-II算法求解多目标VRP问题的Python代码: ```python import numpy as np import random # 定义车辆容量 vehicle_capacity = 10 # 定义货物数量 num_of_customers = 20 # 定义客户坐标 customers = np.random.rand(num_of_customers, 2) # 定义距离矩阵 dist_matrix = np.zeros((num_of_customers, num_of_customers)) for i in range(num_of_customers): for j in range(num_of_customers): dist_matrix[i][j] = np.linalg.norm(customers[i] - customers[j]) # 定义NSGA-II算法参数 pop_size = 100 max_gen = 200 pc = 0.9 pm = 1.0 / num_of_customers alpha = 1 class Individual: def __init__(self): self.chromosome = [] self.fitness = [] self.rank = 0 self.distance = 0 def __lt__(self, other): if self.rank != other.rank: return self.rank < other.rank else: return self.distance > other.distance # 初始化种群 population = [] for i in range(pop_size): ind = Individual() ind.chromosome = [0] + random.sample(range(1, num_of_customers), num_of_customers - 1) population.append(ind) # 定义快速非支配排序函数 def fast_non_dominated_sort(population): S = [[] for i in range(len(population))] front = [[]] n = [0 for i in range(len(population))] rank = [0 for i in range(len(population))] for p in range(len(population)): S[p] = [] n[p] = 0 for q in range(len(population)): if population[p].fitness[0] < population[q].fitness[0] and population[p].fitness[1] < population[q].fitness[1]: if q not in S[p]: S[p].append(q) elif population[q].fitness[0] < population[p].fitness[0] and population[q].fitness[1] < population[p].fitness[1]: n[p] += 1 if n[p] == 0: population[p].rank = 0 if p not in front[0]: front[0].append(p) i = 0 while front[i]: Q = [] for p in front[i]: for q in S[p]: n[q] -= 1 if n[q] == 0: population[q].rank = i + 1 if q not in Q: Q.append(q) i += 1 front.append(Q) return front[:-1] # 定义拥挤度计算函数 def crowding_distance(population, front): for i in range(len(front)): for ind in front[i]: ind.distance = 0 for m in range(len(population[0].fitness)): front = sorted(front, key=lambda ind: ind.fitness[m]) population[front[0]].distance = float('inf') population[front[-1]].distance = float('inf') for i in range(1, len(front) - 1): population[front[i]].distance += (population[front[i + 1]].fitness[m] - population[front[i - 1]].fitness[m]) # 定义选择函数 def selection(population): tournament_size = 2 selected = [] for i in range(pop_size): tournament = random.sample(population, tournament_size) winner = min(tournament) selected.append(winner) return selected # 定义交叉函数 def crossover(parent1, parent2): child1 = Individual() child2 = Individual() child1.chromosome = [-1] * (num_of_customers + 1) child2.chromosome = [-1] * (num_of_customers + 1) r1 = random.randint(1, num_of_customers - 1) r2 = random.randint(r1, num_of_customers - 1) for i in range(r1, r2 + 1): child1.chromosome[i] = parent1.chromosome[i] child2.chromosome[i] = parent2.chromosome[i] j = 0 k = 0 for i in range(num_of_customers): if parent2.chromosome[i + 1] not in child1.chromosome[r1:r2 + 1]: child1.chromosome[j] = parent2.chromosome[i + 1] j += 1 if parent1.chromosome[i + 1] not in child2.chromosome[r1:r2 + 1]: child2.chromosome[k] = parent1.chromosome[i + 1] k += 1 for i in range(num_of_customers): if child1.chromosome[i] == -1: child1.chromosome[i] = parent2.chromosome[i + 1] if child2.chromosome[i] == -1: child2.chromosome[i] = parent1.chromosome[i + 1] child1.chromosome[-1] = 0 child2.chromosome[-1] = 0 return child1, child2 # 定义变异函数 def mutation(individual): for i in range(num_of_customers): if random.random() < pm: j = random.randint(0, num_of_customers - 1) c1 = individual.chromosome[i + 1] c2 = individual.chromosome[j + 1] individual.chromosome[i + 1] = c2 individual.chromosome[j + 1] = c1 return individual # 定义求解函数 def solve(): for i in range(max_gen): offsprings = [] for j in range(int(pop_size / 2)): parent1, parent2 = random.sample(population, 2) if random.random() < pc: child1, child2 = crossover(parent1, parent2) else: child1 = parent1 child2 = parent2 child1 = mutation(child1) child2 = mutation(child2) offsprings += [child1, child2] population += offsprings for ind in population: ind.fitness = [0, 0] for i in range(len(ind.chromosome) - 1): ind.fitness[0] += dist_matrix[ind.chromosome[i]][ind.chromosome[i + 1]] ind.fitness[1] += 1 ind.fitness[1] -= 1 fronts = fast_non_dominated_sort(population) for i in range(len(fronts)): crowding_distance(population, fronts[i]) population = sorted(population, reverse=True) population = population[:pop_size] return population # 调用求解函数 population = solve() # 输出结果 for i in range(len(population)): if population[i].rank == 0: print('Solution {}:'.format(i + 1)) print(' Route: {}'.format(population[i].chromosome)) print(' Distance: {}'.format(population[i].fitness[0])) print(' Num of vehicles: {}'.format(population[i].fitness[1])) ``` 在代码中,先定义了车辆容量、货物数量、客户坐标和距离矩阵等参数。然后定义了Individual类来表示种群中的个体,包括染色体、适应度、排名和拥挤度等属性。接着定义了快速非支配排序和拥挤度计算函数,用于进行多目标优化。最后定义了选择、交叉、变异和求解函数,用于进行遗传算法求解。在求解函数中,使用NSGA-II算法对种群进行进化,并输出最优解的路径和距离等信息。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值