问题描述
我目前正在使用 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 上的聊天徽章)