VRPTW中Depot节点的时间窗与松弛变量处理问题咨询
我来帮你拆解这两个在使用OR-Tools做带时间窗VRP时遇到的常见问题,这些细节确实容易踩坑:
问题1:能否为Depot分别定义出发和到达时间窗?
答案是可以的,但不能直接给同一个depot节点设置单一的时间窗——因为你的路线里depot既是起点又是终点,直接用time_dimension.CumulVar(node).SetRange()会同时约束车辆出发和到达depot的时间,这显然不是你想要的。
这里有两种靠谱的解决思路:
思路1:拆分Depot为“出发节点”和“到达节点”
把原来的depot(比如node 0)拆成两个逻辑节点:
- 用node 0作为车辆的出发 depot,设置它的时间窗为早晨出发时段(比如
(7*60, 8*60)) - 新增一个虚拟节点(比如node 5,假设你原来有5个站点)作为到达 depot,设置它的时间窗为傍晚到达时段(比如
(18*60, 20*60)) - 然后设置车辆的起始节点为0,终止节点为5,同时给这两个节点之间添加一条虚拟路径(成本为0,时间为0)
代码示例大概是这样:
# 假设原来有5个节点(0-4),新增到达depot为5 routing.AddNode(5) # 设置车辆的起始和终止节点 routing.SetStart(vehicle, 0) routing.SetEnd(vehicle, 5) # 添加0到5的虚拟路径(如果需要的话,或者让求解器自动处理) routing.AddEdge(0, 5, 0) # 设置出发depot的时间窗 time_dimension.CumulVar(0).SetRange(7*60, 8*60) # 设置到达depot的时间窗 time_dimension.CumulVar(5).SetRange(18*60, 20*60)
思路2:直接给车辆的起始/结束累积时间加约束
如果你不想新增节点,可以直接针对车辆的起始和结束状态单独加约束,而不是给depot节点加全局时间窗:
# 设置车辆出发depot的时间窗(仅约束起始累积时间) routing.solver().Add( time_dimension.CumulVar(routing.Start(vehicle)) >= 7*60 ) routing.solver().Add( time_dimension.CumulVar(routing.Start(vehicle)) <= 8*60 ) # 设置车辆返回depot的时间窗(约束结束累积时间) routing.solver().Add( time_dimension.CumulVar(routing.End(vehicle)) >= 18*60 ) routing.solver().Add( time_dimension.CumulVar(routing.End(vehicle)) <= 20*60 ) # 然后给其他非depot节点设置时间窗 for node in range(1, 5): time_dimension.CumulVar(node).SetRange(9*60, 10*60)
这种方法更简洁,不需要修改节点结构,直接通过求解器的约束来分别控制出发和到达时间。
问题2:如何正确读取包括Depot在内的所有节点的时间窗松弛变量?
你遇到的SystemError: returned NULL without setting an error,本质原因是:你遍历的indices列表里包含了路由的End虚拟索引,而这个索引对应的不是实际节点,没有对应的松弛变量。
当你用routing.IsEnd(index)判断结束时,最后一个添加到indices里的是routing.End(vehicle)对应的索引,这个索引是OR-Tools内部的虚拟节点,不存在SlackVar,所以调用time_dimension.SlackVar(x)会返回NULL,导致程序崩溃。
解决方法很简单:过滤掉虚拟的End索引,只处理实际存在的节点对应的索引,并且注意Depot节点的松弛变量是否存在(默认情况下,起始节点如果设置了fix_start_cumul_to_zero=True,可能没有松弛变量,需要判断)。
正确的读取代码可以这样写:
# 先获取车辆的实际路线索引(排除End虚拟节点) index = routing.Start(vehicle) valid_indices = [index] while not routing.IsEnd(index): next_index = assignment.Value(routing.NextVar(index)) # 只添加非End的索引 if not routing.IsEnd(next_index): valid_indices.append(next_index) index = next_index # 然后读取每个有效索引对应的松弛变量 slacks = [] for idx in valid_indices: node = routing.IndexToNode(idx) # 检查该节点是否有松弛变量(避免起始节点的情况) try: slack_var = time_dimension.SlackVar(node) slack_val = assignment.Value(slack_var) slacks.append(slack_val) except: # 如果没有松弛变量(比如起始节点),可以添加0或者跳过 slacks.append(0)
另外还要注意:当你设置fix_start_cumul_to_zero=False时,起始节点(depot)是有松弛变量的;如果设置为True,起始节点的累积时间被固定为0,所以没有松弛变量,这时候读取会报错,需要跳过。
内容的提问来源于stack exchange,提问作者Leevi L

