使用 Optaplanner 解决 VRPTWPD

Posted

技术标签:

【中文标题】使用 Optaplanner 解决 VRPTWPD【英文标题】:Using Optaplanner to solve VRPTWPD 【发布时间】:2015-01-18 23:44:41 【问题描述】:

我是 optaplanner 的新手,我希望用它来解决 VRPTW 的取货和交付问题 (VRPTWPD)。

我首先从示例存储库中获取VRPTW code。我正在尝试添加它来解决我的问题。但是,我无法返回符合优先/车辆限制的解决方案(提货必须在交货前完成,并且必须由同一辆车完成)。

我一直返回一个解决方案,其中硬分是我对此类解决方案的期望(即,我可以在一个小样本问题中将所有违规加起来,并查看硬分是否与我为这些违规分配的惩罚相匹配)。

我尝试的第一种方法是按照 Geoffrey De Smet 在这里概述的步骤 - https://***.com/a/19087210/351400

每个Customer 都有一个变量customerType,用于描述它是取货 (PU) 还是送货 (DO)。它还有一个名为parcelId 的变量,用于指示哪个包裹正在被取走或递送。

我向名为parcelIdsOnboardCustomer 添加了一个影子变量。这是一个 HashSet,保存了司机在访问给定的Customer 时随身携带的所有 parcelId。

保持parcelIdsOnboard 更新的我的VariableListener 如下所示:

public void afterEntityAdded(ScoreDirector scoreDirector, Customer customer) 
    if (customer instanceof TimeWindowedCustomer) 
        updateParcelsOnboard(scoreDirector, (TimeWindowedCustomer) customer);
    


public void afterVariableChanged(ScoreDirector scoreDirector, Customer customer) 
    if (customer instanceof TimeWindowedCustomer) 
        updateParcelsOnboard(scoreDirector, (TimeWindowedCustomer) customer);
    


protected void updateParcelsOnboard(ScoreDirector scoreDirector, TimeWindowedCustomer sourceCustomer) 
    Standstill previousStandstill = sourceCustomer.getPreviousStandstill();
    Set<Integer> parcelIdsOnboard = (previousStandstill instanceof TimeWindowedCustomer)
            ? new HashSet<Integer>(((TimeWindowedCustomer) previousStandstill).getParcelIdsOnboard()) : new HashSet<Integer>();

    TimeWindowedCustomer shadowCustomer = sourceCustomer;
    while (shadowCustomer != null) 
        updateParcelIdsOnboard(parcelIdsOnboard, shadowCustomer);
        scoreDirector.beforeVariableChanged(shadowCustomer, "parcelIdsOnboard");
        shadowCustomer.setParcelIdsOnboard(parcelIdsOnboard);
        scoreDirector.afterVariableChanged(shadowCustomer, "parcelIdsOnboard");
        shadowCustomer = shadowCustomer.getNextCustomer();
    


private void updateParcelIdsOnboard(Set<Integer> parcelIdsOnboard, TimeWindowedCustomer customer) 
    if (customer.getCustomerType() == Customer.PICKUP) 
        parcelIdsOnboard.add(customer.getParcelId());
     else if (customer.getCustomerType() == Customer.DELIVERY) 
        parcelIdsOnboard.remove(customer.getParcelId());
     else 
        // TODO: throw an assertion
    

然后我添加了以下流口水规则:

rule "pickupBeforeDropoff"
    when
        TimeWindowedCustomer((customerType == Customer.DELIVERY) && !(parcelIdsOnboard.contains(parcelId)));
    then
        System.out.println("precedence violated");
        scoreHolder.addHardConstraintMatch(kcontext, -1000);
end

对于我的示例问题,我总共创建了 6 个 Customer 对象(3 个 PICKUPS 和 3 个 DELIVERIES)。我的车队规模是 12 辆车。

当我运行此程序时,我始终得到 -3000 的硬分,这与我看到正在使用的两辆车的输出相匹配。一辆车负责所有的接送,一辆车负责所有的送货。

我使用的第二种方法是为每个Customer 提供对其对应对象Customer 的引用(例如,包裹1 的PICKUP Customer 引用了DELIVERY Customer包裹 1,反之亦然)。

然后我实施了以下规则来强制包裹在同一辆车中(注意:没有完全实施优先约束)。

rule "pudoInSameVehicle"
    when
        TimeWindowedCustomer(vehicle != null && counterpartCustomer.getVehicle() != null && (vehicle != counterpartCustomer.getVehicle()));
    then
        scoreHolder.addHardConstraintMatch(kcontext, -1000);
end

对于相同的示例问题,这始终给出 -3000 的分数和与上述问题相同的解决方案。

我已经尝试在FULL_ASSERT 模式下运行这两个规则。使用parcelIdsOnboard 的规则不会触发任何异常。但是,"pudoInSameVehicle" 规则确实会触发以下异常(FAST_ASSERT 模式下不会触发)。

The corrupted scoreDirector has no ConstraintMatch(s) which are in excess.
The corrupted scoreDirector has 1 ConstraintMatch(s) which are missing:

我不确定为什么会损坏,任何建议将不胜感激。

有趣的是,这两种方法都产生了相同的(不正确的)解决方案。我希望有人会对接下来要尝试的内容提出一些建议。谢谢!

更新:

深入研究在 FULL_ASSERT 模式下触发的断言后,我意识到问题在于 PICKUP 和 DELIVERY Customers 的依赖性质。也就是说,如果您采取的行动消除了 DELIVERY Customer 的硬性处罚,您还必须消除与 PICKUP Customer 相关的处罚。为了保持这些同步,我更新了我的VehicleUpdatingVariableListener 和我的ArrivalTimeUpdatingVariableListener 以触发两个Customer 对象的分数计算回调。这是 updateVehicle 方法在更新它以触发对刚刚移动的 Customer 和对应的 Customer 的分数计算之后。

protected void updateVehicle(ScoreDirector scoreDirector, TimeWindowedCustomer sourceCustomer) 
    Standstill previousStandstill = sourceCustomer.getPreviousStandstill();
    Integer departureTime = (previousStandstill instanceof TimeWindowedCustomer)
            ? ((TimeWindowedCustomer) previousStandstill).getDepartureTime() : null;

    TimeWindowedCustomer shadowCustomer = sourceCustomer;
    Integer arrivalTime = calculateArrivalTime(shadowCustomer, departureTime);
    while (shadowCustomer != null && ObjectUtils.notEqual(shadowCustomer.getArrivalTime(), arrivalTime)) 
        scoreDirector.beforeVariableChanged(shadowCustomer, "arrivalTime");
        scoreDirector.beforeVariableChanged(((TimeWindowedCustomer) shadowCustomer).getCounterpartCustomer(), "arrivalTime");
        shadowCustomer.setArrivalTime(arrivalTime);
        scoreDirector.afterVariableChanged(shadowCustomer, "arrivalTime");
        scoreDirector.afterVariableChanged(((TimeWindowedCustomer) shadowCustomer).getCounterpartCustomer(), "arrivalTime");
        departureTime = shadowCustomer.getDepartureTime();
        shadowCustomer = shadowCustomer.getNextCustomer();
        arrivalTime = calculateArrivalTime(shadowCustomer, departureTime);
    

这解决了我在第二种方法中遇到的分数损坏问题,并且在一个小样本问题上,产生了一个满足所有硬约束的解决方案(即该解决方案的硬分值为 0)。

接下来我尝试运行一个更大的问题(约 380 个客户),但解决方案返回的硬分非常差。我尝试寻找解决方案 1 分钟、5 分钟和 15 分钟。分数似乎随着运行时间线性提高。但是,在 15 分钟时,解决方案仍然很糟糕,似乎需要运行至少一个小时才能产生可​​行的解决方案。 我需要它最多在 5-10 分钟内运行。

我了解了Filter Selection。我的理解是,您可以运行一个函数来检查您即将进行的移动是否会导致破坏内置的硬约束,如果会,则跳过此移动。

这意味着您不必重新运行分数计算或探索您知道不会有成果的分支。例如,在我的问题中,我不希望您能够将 Customer 移动到 Vehicle,除非它的对应对象已分配给该车辆或根本没有分配车辆。

这是我为检查而实施的过滤器。它仅适用于 ChangeMoves,但我怀疑我也需要它来为 SwapMoves 实现类似的功能。

public class PrecedenceFilterChangeMove implements SelectionFilter<ChangeMove>  

    @Override
    public boolean accept(ScoreDirector scoreDirector, ChangeMove selection) 
        TimeWindowedCustomer customer = (TimeWindowedCustomer)selection.getEntity();
        if (customer.getCustomerType() == Customer.DELIVERY) 
            if (customer.getCounterpartCustomer().getVehicle() == null) 
                return true;
            
            return customer.getVehicle() == customer.getCounterpartCustomer().getVehicle();
        
        return true;
    

添加此过滤器会立即导致更差的分数。这让我觉得我错误地实现了这个功能,虽然我不清楚为什么它不正确。

更新 2:

一位同事指出了我的 PrecedenceFilterChangeMove 的问题。正确的版本如下。我还包括 PrecedenceFilterSwapMove 实现。总之,这些使我能够在大约 10 分钟内找到不违反硬约束的问题的解决方案。我认为我可以进行其他一些优化来进一步减少这种情况。

如果这些更改卓有成效,我将发布另一个更新。我仍然很想听听 optaplanner 社区中的某个人关于我的方法以及他们是否认为有更好的方法来模拟这个问题!

PrecedenceFilterChangeMove

@Override
public boolean accept(ScoreDirector scoreDirector, ChangeMove selection) 
    TimeWindowedCustomer customer = (TimeWindowedCustomer)selection.getEntity();
    if (customer.getCustomerType() == Customer.DELIVERY) 
        if (customer.getCounterpartCustomer().getVehicle() == null) 
            return true;
        
        return selection.getToPlanningValue() == customer.getCounterpartCustomer().getVehicle();
     
    return true;

PrecedenceFilterSwapMove

@Override
public boolean accept(ScoreDirector scoreDirector, SwapMove selection) 
    TimeWindowedCustomer leftCustomer = (TimeWindowedCustomer)selection.getLeftEntity();
    TimeWindowedCustomer rightCustomer = (TimeWindowedCustomer)selection.getRightEntity();
    if (rightCustomer.getCustomerType() == Customer.DELIVERY || leftCustomer.getCustomerType() == Customer.DELIVERY)       
        return rightCustomer.getVehicle() == leftCustomer.getCounterpartCustomer().getVehicle() ||
                leftCustomer.getVehicle() == rightCustomer.getCounterpartCustomer().getVehicle();
    
    return true;

【问题讨论】:

这个问题比较长。有什么办法概括吗? @GeoffreyDeSmet 这个问题越来越多,因为我试图让它与我所做的更改保持同步。正如标题所述,我正在尝试使用 optaplanner 解决 VRPTWPD 问题。我首先在另一篇文章中遵循了您的建议,但我认为这不是一个好方法。我想出了另一种可行的方法,但速度很慢。在这一点上,我试图弄清楚如何编写一个自定义移动类,它使用 CompositeMove 来移动成对的客户(取货/送货),但运气不佳。有什么例子可以指点我吗? 请量化慢:有多少实体/值给出了每秒平均​​计算次数?要使任何超过 1000 个实体的 VRP 仍能正常扩展,需要就近选择(自 6.2.0.CR1 和 CR2 以来的新功能)。 我会对这样的博文感兴趣 :) 八月,你有没有机会在任何地方分享你的结果?我遇到了很多和你现在一样的问题。 【参考方案1】:

有mixed pickup and delivery VRP experimental code here,它有效。我们还没有完善的开箱即用示例,但我们正在制定长期路线图。

【讨论】:

以上是关于使用 Optaplanner 解决 VRPTWPD的主要内容,如果未能解决你的问题,请参考以下文章

在 OptaPlanner 中使用 PDPTW 并行求解

Optaplanner规划引擎的工作原理及简单示例

Optaplanner规划引擎的工作原理及简单示例

Optaplanner 在使用自定义过滤器类时不会终止

Optaplanner:检查链式规划变量是不是有锚

Optaplanner Easy与增量分数计算速度