在存在顺序约束的情况下,OR-tools 路由无法找到“微不足道”的最佳解决方案

Posted

技术标签:

【中文标题】在存在顺序约束的情况下,OR-tools 路由无法找到“微不足道”的最佳解决方案【英文标题】:OR-tools routing fails to find 'trivial' optimal solution in the presence of order-constraints 【发布时间】:2016-09-15 09:41:01 【问题描述】:

为了更好地理解 or-tools 路由背后的约束编程,我创建了一个 depot 和其他 4 个配置为允许两条路由的节点的玩具示例。

这个想法是车辆从01,然后选择23,继续到4 并返回到0 的车库;车辆选择绿色或红色路径。我的实际问题更复杂,有多个车辆,但有类似的限制。

对于这个例子,我为成本创建了一个欧几里得距离函数:

class Distances:
    def __init__(self):
        self.locations = [
            [-1,  0], # source
            [ 0, -1], # waypoint 1
            [ 0,  1], # waypoint 2
            [ 1,  0], # destination
        ]

    def __len__(self):
        return len(self.locations) + 1

    def dist(self, x, y):
        return int(10000 * math.sqrt(
            (x[0] - y[0]) ** 2 +
            (x[1] - y[1]) ** 2))

    def __call__(self, i, j):
        if i == 0 and j == 0:
            return 0
        if j == 0 or i == 0:
            return 1 # very small distance between depot and non-depot, simulating 0
        return self.dist(self.locations[i - 1], self.locations[j - 1])


distance = Distances()

还有一个l0距离函数来约束顺序:

# l0-distance to add order constraints
class Order:
    def __call__(self, i, j):
        return 0 if i == j else 1


order = Order()

然后我创建模型并尝试解决这个问题:

search_parameters = pywrapcp.RoutingModel.DefaultSearchParameters()
search_parameters.first_solution_strategy = (
        routing_enums_pb2.FirstSolutionStrategy.ALL_UNPERFORMED)

search_parameters.local_search_metaheuristic = routing_enums_pb2.LocalSearchMetaheuristic.SIMULATED_ANNEALING
search_parameters.time_limit_ms = 3000

routing = pywrapcp.RoutingModel(len(distance), 1)

routing.SetArcCostEvaluatorOfAllVehicles(distance)
routing.SetDepot(0)
solver = routing.solver()

routing.AddDimension(order, int(1e18), int(1e18), True, "order")

# Since `ALL_UNPERFORMED` is used, each node must be allowed inactive
order_dimension = routing.GetDimensionOrDie("order")
routing.AddDisjunction([1], int(1e10))
routing.AddDisjunction([2, 3], int(1e10))
routing.AddDisjunction([4], int(1e10))

solver.AddConstraint(order_dimension.CumulVar(1) <= order_dimension.CumulVar(2))
solver.AddConstraint(order_dimension.CumulVar(1) <= order_dimension.CumulVar(3))

solver.AddConstraint(order_dimension.CumulVar(2) <= order_dimension.CumulVar(4))
solver.AddConstraint(order_dimension.CumulVar(3) <= order_dimension.CumulVar(4))

# routing.AddPickupAndDelivery(1, 2)
# routing.AddPickupAndDelivery(1, 3)
# routing.AddPickupAndDelivery(2, 4)
# routing.AddPickupAndDelivery(3, 4)

routing.CloseModelWithParameters(search_parameters)
assignment = routing.SolveWithParameters(search_parameters)

if assignment is not None:
    print('cost: ' + str(assignment.ObjectiveValue()))
    route = []
    index = routing.Start(0)
    while not routing.IsEnd(index):
        route.append(routing.IndexToNode(index))
        index = assignment.Value(routing.NextVar(index))
    for node in route:
        print(' - :2d'.format(node))
else:
    print('nothing found')

所以[1][4] 是析取,以允许ALL_UNPERFORMED 第一个解决方案起作用,而析取[2, 3] 表明应该选择绿色或红色路径。

通过这些析取,求解器找到了解决方案,但如果我添加应该在14 之前访问23,求解器不会访问23全部。为什么会这样?为什么求解器不能找到更优的路线0 -&gt; 1 -&gt; 2/3 -&gt; 4 -&gt; 0 避免int(1e10)[2,3] 的析取惩罚?

编辑:

通过删除它们并添加到Distance.__call__来软约束顺序约束:

if (i == 2 or j == 1) or (i == 3 or j == 1) or (i == 4 or j == 2) or (i == 4 or j == 3):
    return int(1e10)

要惩罚一个不允许的订单,会导致路由0 -&gt; 2 -&gt; 1 -&gt; 4 -&gt; 0。所以我想知道为什么or-tools不会交换12,即使在search_parameters.local_search_operators中明确启用use_swap_activeuse_relocate_neighbors

注意:失败,因为它应该是:

if (i == 2 and j == 1) or (i == 3 and j == 1) or (i == 4 and j == 2) or (i == 4 and j == 3):
    return int(1e10)

结论:搜索空间很小,更好的解决方案在返回解决方案的use_relocate_neighbors附近,但or-tools没有找到它。为什么?

所有代码:

import pandas
import os.path

import numpy
import math
from ortools.constraint_solver import pywrapcp
from ortools.constraint_solver import routing_enums_pb2


class Distances:
    def __init__(self):
        self.locations = [
            [-1,  0], # source
            [ 0, -1], # waypoint 1
            [ 0,  1], # waypoint 2
            [ 1,  0], # destination
        ]

    def __len__(self):
        return len(self.locations) + 1

    def dist(self, x, y):
        return int(10000 * math.sqrt(
            (x[0] - y[0]) ** 2 +
            (x[1] - y[1]) ** 2))

    def __call__(self, i, j):
        if i == 0 and j == 0:
            return 0
        if j == 0 or i == 0:
            return 1 # very small distance between depot and non-depot, simulating 0
        return self.dist(self.locations[i - 1], self.locations[j - 1])


distance = Distances()


# l0-distance to add order constraints
class Order:
    def __call__(self, i, j):
        return 0 if i == j else 1


order = Order()

search_parameters = pywrapcp.RoutingModel.DefaultSearchParameters()
search_parameters.first_solution_strategy = (
        routing_enums_pb2.FirstSolutionStrategy.ALL_UNPERFORMED)

search_parameters.local_search_metaheuristic = routing_enums_pb2.LocalSearchMetaheuristic.SIMULATED_ANNEALING
search_parameters.time_limit_ms = 3000

routing = pywrapcp.RoutingModel(len(distance), 1)

routing.SetArcCostEvaluatorOfAllVehicles(distance)
routing.SetDepot(0)
solver = routing.solver()

routing.AddDimension(order, int(1e18), int(1e18), True, "order")

# Since `ALL_UNPERFORMED` is used, each node must be allowed inactive
order_dimension = routing.GetDimensionOrDie("order")
routing.AddDisjunction([1], int(1e10))
routing.AddDisjunction([2, 3], int(1e10))
routing.AddDisjunction([4], int(1e10))

solver.AddConstraint(
    (routing.ActiveVar(2) == 0)
    or
    (order_dimension.CumulVar(1) <= order_dimension.CumulVar(2))
)
solver.AddConstraint(
    (routing.ActiveVar(3) == 0)
    or
    (order_dimension.CumulVar(1) <= order_dimension.CumulVar(3))
)


solver.AddConstraint(
    (routing.ActiveVar(2) == 0)
    or
    (order_dimension.CumulVar(2) <= order_dimension.CumulVar(4))
)
solver.AddConstraint(
    (routing.ActiveVar(3) == 0)
    or
    (order_dimension.CumulVar(3) <= order_dimension.CumulVar(4))
)

# routing.AddPickupAndDelivery(1, 2)
# routing.AddPickupAndDelivery(1, 3)
# routing.AddPickupAndDelivery(2, 4)
# routing.AddPickupAndDelivery(3, 4)

routing.CloseModelWithParameters(search_parameters)
assignment = routing.SolveWithParameters(search_parameters)

if assignment is not None:
    print('cost: ' + str(assignment.ObjectiveValue()))
    route = []
    index = routing.Start(0)
    while not routing.IsEnd(index):
        route.append(routing.IndexToNode(index))
        index = assignment.Value(routing.NextVar(index))
    for node in route:
        print(' - :2d'.format(node))
else:
    print('nothing found')

【问题讨论】:

【参考方案1】:

@furnon 在 github 上通过 github-issues 列表回答了我的问题:https://github.com/google/or-tools/issues/252#issuecomment-249646587

首先,约束编程在更严格的约束上表现更好,我猜有些东西被搜索得很彻底。特别是,我不得不限制订单维度:

routing.AddDimension(order, int(1e18), int(1e18), True, "order")

更好地约束通过

routing.AddDimension(order, len(distance) + 1 ,len(distance) + 1, True, "order")

随后,不需要检查23 是否处于活动状态,因此我们可以像这样简化顺序约束:

solver.AddConstraint(order_dimension.CumulVar(1) <= order_dimension.CumulVar(2))
solver.AddConstraint(order_dimension.CumulVar(1) <= order_dimension.CumulVar(3))
solver.AddConstraint(order_dimension.CumulVar(2) <= order_dimension.CumulVar(4))
solver.AddConstraint(order_dimension.CumulVar(3) <= order_dimension.CumulVar(4))

正如我在内联版本中所做的那样,但不是在全代码版本中。现在返回一个可行的解决方案。

【讨论】:

以上是关于在存在顺序约束的情况下,OR-tools 路由无法找到“微不足道”的最佳解决方案的主要内容,如果未能解决你的问题,请参考以下文章

自定义约束OR-Tools //约束编程

自定义约束 OR-Tools // 约束编程

OR-Tools:向最小成本流类添加约束?

如何在我们的 MIP 问题中使用 or-tools 设置像 y = max(x1,x2,x3) 这样的等式约束?

OR-Tools / SCIP - 如何使用指标约束来解决 MIP 问题?

在 Java 中使用 google or-tools 进行除法不等式约束