ortools系列:时间窗约束VRP问题

1. VRPTW

许多车辆路径问题都涉及到安排送货时间,这些问题的一个自然约束是,客户只能在指定的时间窗口内接收交付或服务调用,这些问题称为时间窗车辆路径问题(VRPTW)。

下图以蓝色表示客户地点,黑色表示仓库。时间窗口(以分钟为单位)显示在每个位置的上方。

目标是最小化在时间窗口内为所有客户提供所需的服务时间和等待时间之和。

前面我们讲了普通VRP和CVRP问题,这里的VRPTW问题相信你也明白,只需要添加时间窗口约束介面。

from ortools.constraint_solver import pywrapcp
from ortools.constraint_solver import routing_enums_pb2

# 创建数据集
def create_data_model():
data = {}
# 不同客户节点之间的距离
_distances =
[
[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]
]

# 每个客户的需求量,0 表示改节点是仓库,仓库是没有需求
demands = [0, 1, 1, 2, 1, 2, 4, 8, 8, 1, 2, 1, 2, 4, 4, 8, 8]

# 每个节点的时间窗口约束
time_windows =
[(0, 0),
(75, 85),
(75, 85),
(60, 70),
(45, 55),
(0, 8),
(50, 60),
(0, 10),
(10, 20),
(0, 10),
(75, 85),
(85, 95),
(5, 15),
(15, 25),
(10, 20),
(45, 55),
(30, 40)]

# 距离矩阵
data["distances"] = _distances
# 网路节点数量
data["num_locations"] = len(_distances)
# 车辆数量
data["num_vehicles"] = 4
# 仓库索引,我们假设所有的车辆都从同一地点出发,也就是车场。
# 或者,你可以允许车辆在任何位置启动和结束。
data["depot"] = 0
# 需求
data["demands"] = demands
# 时间窗
data["time_windows"] = time_windows
# 假设每个客户的服务时间的5分钟
data["time_per_demand_unit"] = 5
# 车辆平均行驶速度
data["vehicle_speed"] = 83.33
return data

#######################
# Problem Constraints #
#######################
def create_distance_callback(data):
"""Creates callback to return distance between points."""
distances = data["distances"]

def distance_callback(from_node, to_node):
"""Returns the manhattan distance between the two nodes"""
return distances[from_node][to_node]

return distance_callback

# 时间窗口约束的回调函数
def create_time_callback(data):
"""Creates callback to get total times between locations."""

def service_time(node):
"""Gets the service time for the specified location."""
return data["demands"][node] * data["time_per_demand_unit"]

def travel_time(from_node, to_node):
"""Gets the travel times between two locations."""
travel_time = data["distances"][from_node][to_node] / data["vehicle_speed"]
return travel_time

def time_callback(from_node, to_node):
"""Returns the total time between the two nodes"""
serv_time = service_time(from_node)
trav_time = travel_time(from_node, to_node)
return serv_time + trav_time

return time_callback

# 和容量约束的维度约束类似,这里需要添加时间窗口的约束,也是用 routing.AddDimension 函数
def add_time_window_constraints(routing, data, time_callback):
"""Add time window constraints."""
time = "Time"
horizon = 120
routing.AddDimension(
time_callback,
horizon, # allow waiting time
horizon, # maximum time per vehicle
False, # Dont force start cumul to zero. This doesnt have any effect in this example,
# since the depot has a start window of (0, 0).
time)

time_dimension = routing.GetDimensionOrDie(time)
for location_node, location_time_window in enumerate(data["time_windows"]):
index = routing.NodeToIndex(location_node)
time_dimension.CumulVar(index).SetRange(location_time_window[0], location_time_window[1])

# 列印结果
def print_solution(data, routing, assignment):
"""Prints assignment on console"""
# Inspect solution.
time_dimension = routing.GetDimensionOrDie(Time)
total_dist = 0
time_matrix = 0

for vehicle_id in range(data["num_vehicles"]):
index = routing.Start(vehicle_id)
plan_output = Route for vehicle {0}:
.format(vehicle_id)
route_dist = 0
while not routing.IsEnd(index):
node_index = routing.IndexToNode(index)
next_node_index = routing.IndexToNode(
assignment.Value(routing.NextVar(index)))
route_dist += routing.GetArcCostForVehicle(node_index, next_node_index, vehicle_id)
time_var = time_dimension.CumulVar(index)
time_min = assignment.Min(time_var)
time_max = assignment.Max(time_var)
plan_output += {0} Time({1},{2}) ->.format(node_index, time_min, time_max)
index = assignment.Value(routing.NextVar(index))

node_index = routing.IndexToNode(index)
time_var = time_dimension.CumulVar(index)
route_time = assignment.Value(time_var)
time_min = assignment.Min(time_var)
time_max = assignment.Max(time_var)
total_dist += route_dist
time_matrix += route_time
plan_output += {0} Time({1},{2})
.format(node_index, time_min, time_max)
plan_output += Distance of the route: {0} m
.format(route_dist)
plan_output += Time of the route: {0} min
.format(route_time)
print(plan_output)
print(Total Distance of all routes: {0} m.format(total_dist))
print(Total Time of all routes: {0} min.format(time_matrix))

# 主函数
def main():

# 创建数据集
data = create_data_model()

# 创建VRP路由模型
routing = pywrapcp.RoutingModel(data["num_locations"], data["num_vehicles"], data["depot"])

# Define weight of each edge
# 提供距离回调,以便解决程序可以计算位置之间的距离。
distance_callback = create_distance_callback(data)
routing.SetArcCostEvaluatorOfAllVehicles(distance_callback)

# Add Time Window constraint
# 添加时间窗口约束
time_callback = create_time_callback(data)
add_time_window_constraints(routing, data, time_callback)

# Setting first solution heuristic (cheapest addition).
# 必须指定启发式方法来找到第一个解决方案
search_parameters = pywrapcp.RoutingModel.DefaultSearchParameters()
search_parameters.first_solution_strategy = (
routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)

# Solve the problem.
# 求解问题
assignment = routing.SolveWithParameters(search_parameters)
if assignment:
printer = print_solution(data, routing, assignment)

if __name__ == __main__:
main()

# 结果
Route for vehicle 0:
0 Time(0,0) -> 12 Time(5,13) -> 13 Time(17,25) -> 15 Time(45,52) -> 11 Time(88,95) -> 0 Time(99,120)
Distance of the route: 1780 m
Time of the route: 99 min

Route for vehicle 1:
0 Time(0,3) -> 5 Time(3,6) -> 8 Time(15,18) -> 6 Time(57,60) -> 2 Time(80,85) -> 0 Time(94,120)
Distance of the route: 1712 m
Time of the route: 94 min

Route for vehicle 2:
0 Time(0,8) -> 7 Time(2,10) -> 4 Time(46,55) -> 3 Time(60,70) -> 1 Time(75,85) -> 0 Time(86,120)
Distance of the route: 1552 m
Time of the route: 86 min

Route for vehicle 3:
0 Time(0,8) -> 9 Time(2,10) -> 14 Time(10,18) -> 16 Time(32,40) -> 10 Time(76,85) -> 0 Time(92,120)
Distance of the route: 1552 m
Time of the route: 92 min

Total Distance of all routes: 6596 m
Total Time of all routes: 371 min

注意add_time_window_constraints函数中的routing.AddDimension调用,在容量约束问题中我们调用了routing.AddDimensionWithVehicleCapacity,相应的,在时间约束问题中,也要在相应的维度上做约束。

为了创建时间维度,我们需要计算如下:

  • 服务时间:在每个地点交付或提供服务所需的时间,这里我们假设服务时间与该位置的需求成正比。
  • 地点之间行驶时间:我们假设所有的车辆以相同的速度行驶,所以行驶时间就是地点之间的距离除以速度。
  • 地点间总时间:即起始位置的服务时间加上到下一个位置的旅行时间。

这三个时间分别对应时间回调函数中的:service_time(node),travel_time(from_node, to_node),time_callback(from_node, to_node).

然后我们看如何添加时间窗口的维度约束。和容量约束的维度约束类似,这里需要添加时间窗口的约束,也是用 routing.AddDimension 函数,看下面的代码片段:

routing.AddDimension(
time_callback,
horizon, # allow waiting time
horizon, # maximum time per vehicle
False, # Dont force start cumul to zero. This doesnt have any effect in this example,
# since the depot has a start window of (0, 0).
time)

AddDimension方法的参数如下:

  • time_evaluator :返回服务时间加上到下一个位置的旅行时间。
  • horizon :每辆车行驶路线累计时间的上限,这将为所有位置设置一个全局时间窗口[0,horizon],要在每个位置设置单独的时间窗口,需要在时间的累积变数上设置范围。
  • 第四个参数和CVRP问题一个意思,由于该值为False,所以在每辆车的路线开始时,时间的累积变数没有设置为零。
  • time :维度的名称,可用于访问其中存储的数据或变数。

2. 参考

  • allow-vehicles-to-start-and-end-any-location
  • google_ortools_guide

大家看完记得关注点赞.

master苏.


推荐阅读:
相关文章