OptaPlanner VRP中用Constraint Streams实现insertLogical的方法问询
在Constraint Streams中实现类似insertLogical的逻辑或外连接方案
一、用leftJoin实现外连接(覆盖首次配送场景)
Constraint Streams支持leftJoin操作,正好能解决你没法包含首次配送的问题。具体思路是:
- 把同一客户的所有配送按时间排序,用
leftJoin将每个配送和它的前序配送关联(首次配送的前序为null) - 对于前序为
null的首次配送,直接用当前储罐状态计算需求;对于有前序的配送,用间隔天数×消耗速率计算需求
示例代码结构:
Constraint demandCalculation(ConstraintFactory constraintFactory) { return constraintFactory.forEach(Delivery.class) .groupBy(Delivery::getCustomer, Collectors.toList()) .flatMap((customer, deliveryList) -> { // 按配送时间排序 List<Delivery> sortedDeliveries = deliveryList.stream() .sorted(Comparator.comparing(Delivery::getDeliveryTime)) .toList(); // 生成每个配送与前序的配对,首次配送前序为null List<Tuple2<Delivery, Delivery>> pairs = new ArrayList<>(); for (int i = 0; i < sortedDeliveries.size(); i++) { Delivery current = sortedDeliveries.get(i); Delivery previous = i == 0 ? null : sortedDeliveries.get(i - 1); pairs.add(Tuple2.of(current, previous)); } return pairs; }) .filter((current, previous) -> current.isScheduled()) .penalize("Demand calculation mismatch", HardSoftScore.ONE_HARD, (current, previous) -> { double expectedDemand; if (previous == null) { // 首次配送:用当前储罐剩余量计算需要补充的量 expectedDemand = current.getCustomer().getTankCapacity() - current.getCustomer().getCurrentTankLevel(); } else { // 非首次配送:计算间隔天数×消耗速率 long daysBetween = ChronoUnit.DAYS.between(previous.getDeliveryTime(), current.getDeliveryTime()); expectedDemand = daysBetween * current.getCustomer().getConsumptionRate(); } // 对比实际配送量和预期需求,返回差值作为惩罚 return Math.abs(current.getDeliveryQuantity() - expectedDemand); }); }
二、用ifExists模拟insertLogical的逻辑
如果你需要类似DRL中insertLogical的“条件满足时激活约束”的效果,可以用Constraint Streams的ifExists操作:
- 对于非首次配送,检查是否存在前序配送,存在时激活需求计算约束
- 对于首次配送,直接激活基于当前状态的需求约束
示例代码:
Constraint firstDeliveryDemand(ConstraintFactory constraintFactory) { // 首次配送:无前序配送的情况 return constraintFactory.forEach(Delivery.class) .ifNotExists(Delivery.class, Joiners.equal(Delivery::getCustomer), Joiners.lessThan(Delivery::getDeliveryTime)) .penalize("First delivery demand mismatch", HardSoftScore.ONE_HARD, delivery -> { double expected = delivery.getCustomer().getTankCapacity() - delivery.getCustomer().getCurrentTankLevel(); return Math.abs(delivery.getDeliveryQuantity() - expected); }); } Constraint subsequentDeliveryDemand(ConstraintFactory constraintFactory) { // 非首次配送:存在前序配送的情况 return constraintFactory.forEach(Delivery.class) .ifExists(Delivery.class, Joiners.equal(Delivery::getCustomer), Joiners.lessThan(Delivery::getDeliveryTime)) .join(Delivery.class, Joiners.equal(Delivery::getCustomer), Joiners.lessThan(second(Delivery::getDeliveryTime), first(Delivery::getDeliveryTime))) .groupBy(first(Delivery.class), Collectors.maxBy(Comparator.comparing(Delivery::getDeliveryTime))) .penalize("Subsequent delivery demand mismatch", HardSoftScore.ONE_HARD, (current, previousOpt) -> { Delivery previous = previousOpt.orElseThrow(); long daysBetween = ChronoUnit.DAYS.between(previous.getDeliveryTime(), current.getDeliveryTime()); double expected = daysBetween * current.getCustomer().getConsumptionRate(); return Math.abs(current.getDeliveryQuantity() - expected); }); }
三、替代预调度配送的简化方案
如果你之前用预调度配送的方式遇到车辆限制问题,可以改为:
- 给每个客户的“虚拟首次配送”设置一个固定的、早于所有规划时间的时间点,且不分配车辆
- 在约束中直接将这个虚拟配送作为首次配送的前序,这样所有配送都有配对,无需处理
null情况 - 同时添加约束确保这个虚拟配送不会被实际调度(比如固定其状态为已完成,不参与车辆分配)
这样既避免了外连接的问题,又不用处理复杂的车辆限制逻辑。
内容的提问来源于stack exchange,提问作者elk-tamer
相关产品推荐
相关产品推荐

