之前我们已经介绍了TSP、VRP、CVRP,今天我们再介绍另一种VRP的应用场景:带提货和送货的VRP,之前的CVRP是只提货不送货的场景。
带取货和送货的 VRP 示例
这里我们的每辆车需要在不同的地点提取货物并在其他地点交付货物。我们的要求是为车辆分配路线以提取和交付所有货物,同时最小化所有访问路线的总长度。下图网格上显示了取货和送货地点,这里的地点和之前我们介绍的CVRP案例中的地点相同,只不过我们在此基础上增加了取货和送货的路径(从取货地点到交货地点之间有一条有向边)。
使用 OR-Tools 解决示例
以下部分描述了如何通过取货和送货解决 VRP的问题。大部分代码是之前面的 VRP 案例中的类似 ,因此我们将重点介绍新增部分的代码逻辑。
创建数据模型
与之前的VRP的数据模型结构类似,只不过这里我们新增了一个条目:“pickups_deliveries”, 用来记录取货和送货的地点索引。
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['pickups_deliveries'] = [
[1, 6],
[2, 10],
[4, 3],
[5, 9],
[7, 8],
[15, 11],
[13, 12],
[16, 14],
]
data['num_vehicles'] = 4
data['depot'] = 0
return data
data = create_data_model()
对于data['pickups_deliveries']中的每一个条目都是一个list结构,list[0]表示取货地点的索引,list[1]表示送货地点的索引。
创建路由模型
以下代码在程序的主要部分创建了索引管理器(manager)和路由模型(routing)。 manager主要根据distance_matrix来管理各个城市的索引,而routing用来计算和存储访问路径。
manager = pywrapcp.RoutingIndexManager(len(data['distance_matrix']),
data['num_vehicles'], data['depot'])
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)
定义距离维度
这里距离维度的说明与VRP一样,大家可以参考我之前写的这篇博客,这里不在赘述。
dimension_name = 'Distance'
routing.AddDimension(
transit_callback_index,
0, # no slack
3000, # vehicle maximum travel distance
True, # start cumul to zero
dimension_name)
distance_dimension = routing.GetDimensionOrDie(dimension_name)
distance_dimension.SetGlobalSpanCostCoefficient(100)
定义取货和送货请求
这里需要说明三点:
- 对于data['pickups_deliveries']中的每一对提货,送货的条目,我们都需要在routing的AddPickupAndDelivery的方法添加提货和送货的请求。
- 我们还要保证提货和送货都是由同一辆车完成的,因此我们必须要添加routing.VehicleVar变量来确保提货和送货是同一辆车。
- 我们要确保在送货之前货物必须先被提取,因此这里要设置一个累积行驶距离(CumulVar
)的约束,即取货的累积行驶距离必须小于等于送货的累积行驶距离,这样就可以保证送货之前先完成取货。
for request in data['pickups_deliveries']:
pickup_index = manager.NodeToIndex(request[0])
delivery_index = manager.NodeToIndex(request[1])
routing.AddPickupAndDelivery(pickup_index, delivery_index)
routing.solver().Add(
routing.VehicleVar(pickup_index) == routing.VehicleVar(
delivery_index))
routing.solver().Add(
distance_dimension.CumulVar(pickup_index) <=
distance_dimension.CumulVar(delivery_index))
添加打印路基输出函数
def print_solution(data, manager, routing, solution):
"""Prints solution on console."""
print(f'Objective: {solution.ObjectiveValue()}')
total_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)
total_distance += route_distance
print('Total Distance of all routes: {}m'.format(total_distance))
设置搜索策略
search_parameters = pywrapcp.DefaultRoutingSearchParameters()
search_parameters.first_solution_strategy = (routing_enums_pb2.FirstSolutionStrategy.PARALLEL_CHEAPEST_INSERTION)
这里我们也需要设置 first_solution_strategy:PARALLEL_CHEAPEST_INSERTION,关于PARALLEL_CHEAPEST_INSERTION策略,大家可以参考官方文档。
模型求解
solution = routing.SolveWithParameters(search_parameters)
if solution:
print_solution(data, manager, routing, solution)
总结
- 在该案例中因为涉及到取货和送货,因此这个需求对应的约束就是:车辆必须按指定的部分路线行驶。通过AddPickupAndDelivery方法在约束条件中加入取货和送货的信息。
- 取货送货的逻辑中包含了2个隐藏约束:1)送货之前货物必须先被提取. 2)必须保证同一辆车来实现取货和送货,为此需要增加两个额外的约束(VehicleVar和CumulVar)来实现这两个隐藏的约束条件。
- 我们使用了和之前不同的搜索策略:PARALLEL_CHEAPEST_INSERTION。