Google OR-tools VRP - 在同一节点上取车/下车

问题描述

我目前正在使用 Python 和 Google-Or-Tools 来解决 VRP 问题,但我不确定如何准确建模。 (如果有人对另一个库/工具有解决方案,也绝对感兴趣)

问题陈述:

我基本上已经在 documentation page 上描述和建模了经典的 CVRP 问题,但增加一个。 所以基本的 CVRP 问题是,我有一个仓库,货物装载和车辆开始/结束。我也有将货物运送到的地点,例如他们有一个需求。 现在补充的是,我不仅要在特定地点下车,而且还想在同一地点取货,最终再次在仓库下车。

由于一个位置可能有更多的货物需要取货而不是卸货,因此我也需要明确检查这一点......但到目前为止我找不到办法做到这一点。

示例: 假设我有一个 Depot 节点 0 和两个位置节点 (A,B)。

  • 在位置 A 我需要下车 10 个单位并取回 11 个单位。
  • 在位置 B 我需要下车 10 个单位并取 9 个单位。

现在对于最大容量为 20 的车辆,可能的解决方案是:

  • 0 --> B --> A --> 0

先访问 A 再访问 B 是行不通的,因为这会违反 A 中的容量限制。

我尝试过的:

所以要考虑基本的下车容量限制,我有标准

routing.AddDimensionWithVehicleCapacity(
    dropoff_callback_index,# null capacity slack
    data['vehicle_capacities'],# vehicle maximum capacities
    True,# start cumul to zero
    'Capacity')

但是,对于取件,我当然可以根据取件需求添加一个。但显然这还不够,但我需要将这两者结合起来。 由于从技术上讲,模型在节点上累积了体积,并且只知道车辆在形成完整行程后从仓库出发的容量,我无法构建这样一个维度,可以在运行节点时检查约束,但是只有在他找到完整的旅行之后。

所以这就是我认为的一种可能的方法,在找到解决方案后进行检查,如果当时已知的车辆在仓库中装载的数量(即路线的下车总和)适合考虑皮卡并且不违反车辆容量。但不确定如何做到这一点。这里有人有想法吗?

我还尝试使用取货和送货模型对其进行建模,并将具有取货和送货的一批货物拆分为两批货物,还将节点复制为具有两个节点而不是一个节点(因为一个节点不能同时进行取货和送货)。但是,由于我的旅行都是从仓库/到仓库开始,我还需要复制仓库节点,然后为它们附加上/下车需求,这没有意义,因为我无法提前设置,但模型应该会找到解决方案(希望这对您有意义)。

我尝试搜索类似的问题(在这里,googlegroups、github),但没有找到任何有用的东西。

解决方法

IIRC,已经在邮件列表上回复了。
这里有一个示例要点:Mizux/vrp_collect_deliver.py

基本上你需要两个维度。

  • 一个只跟踪交付货物
  • 跟踪总负载(取货和送货)
  • 对起始节点的一个约束使两个维度累积变量相等。

主要思想: 使用二维而不是一维,将避免求解器使用提货来执行交付。

字段 开始 A B 结束
交货 0 -10 -10 0
总计 0 -10+11 -10+9 0
deliveries = [0,-10,0]
total = [0,+1,-1,0]

...

# Add Deliveries constraint.
def delivery_callback(from_index):
    """Returns the demand of the node."""
    # Convert from routing variable Index to demands NodeIndex.
    from_node = manager.IndexToNode(from_index)
    return deliveries[from_node]
    
delivery_callback_index = routing.RegisterUnaryTransitCallback(delivery_callback)
routing.AddDimensionWithVehicleCapacity(
    delivery_callback_index,# null capacity slack
    20,# vehicle maximum capacities
    False,# start_cumul_to_zero=False since we start full of goods to deliver
    'Deliveries')

# Add Load constraint.
def load_callback(from_index):
    """Returns the load of the node."""
    # Convert from routing variable Index to demands NodeIndex.
    from_node = manager.IndexToNode(from_index)
    return total[from_node]

load_callback_index = routing.RegisterUnaryTransitCallback(load_callback)
routing.AddDimensionWithVehicleCapacity(
    load_callback_index,# start_cumul_to_zero=False
    'Loads')

    # Add Constraint Both cumulVar are identical at start
    deliveries_dimension = routing.GetDimensionOrDie('Deliveries')
    loads_dimension = routing.GetDimensionOrDie('Loads')
    for vehicle_id in range(manager.GetNumberOfVehicles()):
      index = routing.Start(vehicle_id)
      routing.solver().Add(
          deliveries_dimension.CumulVar(index) == loads_dimension.CumulVar(index))

...
,

免责声明:以上@Tobgen 问题的回答:

当 start_cumul_to_zero=False 时,我认为这就像卡车满载一样启动。

如果 start_cumul_to_zero=True 求解器将简单地强制起始节点为 0: 相当于:

for v in range(manager.GetNumberOfVehicles()):
  index = routing.Start(v)
  dim.CumulVar(index).SetValue(0)

否则它将保持其原始范围[0-capacity](编辑此处[0,20])即一开始,所有变量都有一个范围,但不是一个固定值。

实际上,求解器的目标是修复所有变量以找到解决方案,同时尊重所有约束(因此称为约束编程;))...

所以假设您访问第一个节点 A,求解器会“认为”好,我有 10 件货物,所以我在开始时至少需要 10 件货物,因此我将仓库变量的范围更新为 [10,20], A 现在在 [0,10] 范围内。 然后稍后尝试添加 B,好吧,我需要在 A 中至少 10 个货物才能将货物运送到 B,因此 A 的新范围是 [10,10],这也意味着开始范围是 [20,20],B 是 {{1} } 等等...

它只是说对于起始节点(即 0,因为所有车辆都开始它们),两个维度都应具有相同的值。但这不是已经被最大车辆容量和 [0,0] 的事实暗示了吗?

No start_cumul_to_zero=False 只能避免将 start 固定为 0 否则求解器可以自由地将其固定为域 start_cumul_to_zero=False

中的任何

没有这个约束求解器可以随意使用它想要的任何东西。
例如20 交付,因为您有两个 -10 交付(并且 CumulVar 不能为负!) 但是也可以自由地将 Total 设置为 0,这样 [0,capacity] 就可以了。

见:

维度 开始 B A 结束
发货 20 10 (-10) 0(-10) 0
总负载 0 1(-10+11) 0(-10+9) 0

但是,有了约束,您将:

维度 开始 B
发货 20 10 (-10)
总负载 交付(开始)又名 20 21 个问题! (-10+11)

ps:您也可以在 Discord 上加入我们(请参阅 README.md on github 上的聊天徽章)