返回

OptaPlanner: 事实顺序影响解决方案?避坑指南

java

ProblemFactCollectionProperty 中事实的顺序影响 OptaPlanner 的解决方案

在使用 OptaPlanner 解决规划问题时,ProblemFactCollectionProperty 用于将一组事实传递给求解器。 这些事实可能包括各种约束条件、资源或配置选项。通常情况下,我们认为这些事实的集合是一个无序的集合,也就是通常使用 Set 集合,其内部元素的顺序不应该影响最终解决方案的质量。然而,某些情况下事实的排列顺序实际上会对 OptaPlanner 找到可行方案的能力产生不利的影响。 本文会深入探讨这个问题,并提供一些有效的解决方案。

问题分析

问题的场景展示了一种罕见但潜在的情况,当 ProblemFactCollectionProperty 使用 Set 来管理 Money 事实时,OptaPlanner 可能会无法找到解决方案。 而如果将 Set 替换为 List,问题消失。 更让人困惑的是,对 List 中的元素进行排序,这个问题又重新出现。 这暗示 OptaPlanner 的行为对事实集合的内部顺序相当敏感,这明显与通常的理解相悖。

这个问题的根本原因在于 OptaPlanner 的搜索算法和评估过程,特别是与约束的评估方式有关。 如果约束评估函数在早期阶段遇到了某些特定顺序的事实,它可能会过早地剪枝掉搜索空间的一部分,导致求解器错失最优解或者可行解。

另一个需要考虑的因素是 OptaPlanner 使用的启发式算法。不同的事实顺序可能导致启发式算法以不同的方式探索搜索空间。 某些顺序可能引导算法进入局部最优区域,而难以逃脱,或者导致评估函数过早地给出负面反馈,阻止其探索更有希望的区域。

解决方案

以下是一些解决 ProblemFactCollectionProperty 中事实顺序敏感问题的策略:

1. 确保约束的鲁棒性:

约束的设计目标应当是不依赖于事实的特定排列顺序。 避免编写依赖于特定事实顺序的约束条件。

  • 策略: 重新检查所有的约束条件,确保约束的评估逻辑不会因为事实的排列顺序不同而产生偏差。 使用聚合函数,例如 sum(), average(), min(), max(),这些函数忽略输入的顺序。 如果实在无法避免,可以考虑引入中间变量或者对事实进行预处理,以消除顺序依赖性。
  • 代码示例 (Drools Rule):
    rule "Avoid Order Dependent Constraint"
    when
      $factCollection : List() from collect(Money()) // Collect facts into a list (order matters)
      // BAD: Directly use indices which depends on fact collection's order.
      eval( $factCollection.get(0).isGreaterThan($factCollection.get(1)) )
    
      // GOOD: Using aggregation to calculate sum for Money values in list (order irrelevant).
      Number(intValue() > 1000) from accumulate (
          $money : Money() from $factCollection,
          sum( $money.getAmount().intValue() )
      )
    then
      // Your logic
    end
    
    上面的规则展示了一个依赖顺序的反例以及一种规避此问题的修改版本,即从依赖具体顺序索引取值改进为采用不依赖顺序的累加计算。

2. 使用 List 代替 Set,并引入随机化:

  • 策略:ProblemFactCollectionProperty 的类型从 Set 改为 List。 在 OptaPlanner 启动求解过程之前,对 List 中的事实进行随机排序。 多次运行 OptaPlanner,每次都使用不同的随机顺序。通过这种方法,可以降低因特定顺序而导致问题无法找到最佳解决方案的风险。

  • 代码示例 (Java):

    @ProblemFactCollectionProperty
    @ValueRangeProvider(id = "amountFacts")
    private List<Money> amountFacts;
    
    public void beforeSolve() {
        // 启动求解器前随机打乱集合元素顺序
        Collections.shuffle(amountFacts);
    }
    
  • 操作步骤:

    1. 在你的 Planning Solution 类中,添加一个 beforeSolve() 方法。
    2. 在这个方法中,调用 Collections.shuffle(amountFacts)amountFacts 列表进行洗牌。
    3. 配置 OptaPlanner 多次运行求解器,每次运行都使用不同的随机种子。可以在 OptaPlanner 的配置 XML 文件中指定 solverEventListenerConfigrandomSeed
    4. 如果可行的话,可以考虑实现 SolverEventListener 在每次运行结束时记录当前的随机种子,方便问题复现。

3. 定制 ValueRangeProvider:

  • 策略: 使用自定义的 ValueRangeProvider,将值提供器的实现从简单的字段访问转变为动态生成。在这个自定义的 ValueRangeProvider 中,可以实现排序或者随机化值范围的功能。
  • 代码示例 (Java):
    public class CustomMoneyRangeProvider implements ValueRangeProvider<Money> {
    
        private List<Money> moneyList;
    
        public CustomMoneyRangeProvider(List<Money> moneyList) {
            this.moneyList = moneyList;
        }
    
        @Override
        public List<Money> getValueRange(ScoreDirector scoreDirector, Object planningEntity) {
             List<Money> shuffledList = new ArrayList<>(moneyList);
             Collections.shuffle(shuffledList); // 随机排序
             return shuffledList;
        }
    }
    
    // 在Planning Entity 中引用自定义的 ValueRangeProvider
    @PlanningVariable(valueRangeProviderRef = "customMoneyRangeProvider")
    private Money amount;
    
    //在构建Solver的时候, 添加customMoneyRangeProvider实例
    .withValueRangeProvider("customMoneyRangeProvider",new CustomMoneyRangeProvider(moneyList))
    
    
  • 操作步骤:
    1. 创建一个实现了 ValueRangeProvider 接口的类,并实现 getValueRange 方法。
    2. getValueRange 方法中,从你的事实集合中创建一个新的 List,并使用 Collections.shuffle() 对其进行随机排序。
    3. 将这个自定义的 ValueRangeProvider 配置到你的 PlanningVariable 上。

4. 探索不同的搜索阶段配置:

  • 策略: OptaPlanner 允许配置多个搜索阶段,例如 First Fit, Local Search, Tabu Search 等。 尝试调整搜索阶段的顺序、参数和算法,例如修改接受器、步数等。 不同的搜索配置对事实的顺序敏感性可能有所不同。
  • 代码示例 (OptaPlanner 配置文件):
    <solver>
      <!-- ... -->
      <phases>
        <constructionHeuristic>
          <constructionHeuristicType>FIRST_FIT</constructionHeuristicType>
        </constructionHeuristic>
        <localSearch>
          <acceptor>
            <simulatedAnnealing/>
          </acceptor>
          <forager>
            <acceptedCountLimit>400</acceptedCountLimit>
          </forager>
        </localSearch>
      </phases>
    </solver>
    
  • 操作步骤:
    1. 修改 OptaPlanner 的 solver configuration 文件。
    2. 尝试不同的 <constructionHeuristicType>, 例如 FIRST_FIT_DECREASING, ALLOCATE_ENTITY_FROM_QUEUE等。
    3. 调整 <localSearch> 中的 <acceptor><forager> 参数,例如调整 acceptedCountLimit 的值。
    4. 也可以尝试增加 Simulated Annealing 的初始温度和降温速率,改变算法的探索方式。

额外建议

在处理这类问题时,建议采取以下安全措施:

  • 保持事实集合的精简: 尽量减少 ProblemFactCollectionProperty 中事实的数量,只包含必要的信息。
  • 数据校验: 对输入的数据进行校验,确保其有效性和一致性,以减少求解器处理异常数据的可能性。
  • 监控与日志: 增加日志输出,记录 OptaPlanner 在求解过程中的关键决策和评估结果,有助于分析问题的原因。
  • 基准测试: 在修改配置或代码后,进行基准测试,比较不同方案的性能,确保改进不会引入新的问题。

通过对问题本质的理解和以上方案的灵活应用,可以有效规避 OptaPlanner 中 ProblemFactCollectionProperty 事实顺序敏感问题,并找到更加可靠的解决方案。