SolverConfig多实体类配置错误及带时间约束的医生就诊调度问题求助
SolverConfig多实体类配置错误及带时间约束的医生就诊调度问题求助
兄弟我太懂这种卡壳的感觉了!你这个需求本质上是带时间窗+多约束的路径规划问题,和经典VRP(车辆路径问题)几乎一模一样——只不过把“配送车辆”换成了“出诊医生”,“配送任务”换成了“患者就诊”,核心难点就是多规划实体的配置容易踩坑,加上时间、驾驶时长这些约束的叠加。
先帮你理清楚核心逻辑,再给你踩坑后的解决方案:
一、先搞对规划实体的定义(别搞混多实体!)
你提到的“多个规划实体”其实是个误区——不需要把医生(Provider)设为规划实体,正确的设定应该是:
- 核心规划实体:
Visit(每个患者的就诊请求是一个待分配的任务,需要确定两个关键信息:分配给哪个医生,以及在医生日程中的顺序) - 辅助虚拟实体:给每个医生加一个
ProviderStartVisit(虚拟的“出发任务”,地点是医生家,时间是医生上班起始点),用来锚定医生的日程起点,方便串联后续就诊的时间线 - 规划值:
Provider(作为可分配的资源),以及Visit(用来构建就诊顺序的链式变量)
二、SolverConfig的关键配置要点
1. 规划变量的定义(伪代码示例)
给Visit类加上正确的注解,明确两个规划变量:
@PlanningEntity public class Visit { // 患者的基本信息和时间窗 private Member member; private LocalDateTime memberTimeWindowStart; private LocalDateTime memberTimeWindowEnd; private Duration visitDuration = Duration.ofMinutes(55); // 固定就诊时长 private Location location; // 患者位置 // 规划变量1:分配给哪个医生 @PlanningVariable(valueRangeProviderRefs = "providerRange") private Provider provider; // 规划变量2:前一个就诊任务(用来构建医生的日程链) @PlanningVariable(valueRangeProviderRefs = "visitRange") private Visit previousVisit; // 计算就诊实际开始时间:前一个任务结束时间 + 两地驾驶时长 public LocalDateTime getActualStartTime() { if (previousVisit == null) { // 如果是虚拟起始点,直接用医生的上班开始时间 return provider.getAvailabilityStart(); } Duration driveTime = calculateDriveTime(previousVisit.getLocation(), this.location); return previousVisit.getActualEndTime().plus(driveTime); } // 计算就诊实际结束时间 public LocalDateTime getActualEndTime() { return getActualStartTime().plus(visitDuration); } } // 医生的虚拟起始任务 public class ProviderStartVisit extends Visit { public ProviderStartVisit(Provider provider) { this.provider = provider; this.location = provider.getHomeLocation(); this.memberTimeWindowStart = provider.getAvailabilityStart(); this.memberTimeWindowEnd = provider.getAvailabilityEnd(); } }
2. 核心约束的实现(用OptaPlanner的ConstraintStream)
这部分是灵魂,要把你提到的所有约束都落地:
public class VisitSchedulingConstraintProvider implements ConstraintProvider { @Override public Constraint[] defineConstraints(ConstraintFactory constraintFactory) { return new Constraint[] { // 硬约束:就诊必须在患者的时间窗内完成 memberTimeWindowViolation(constraintFactory), // 硬约束:就诊必须在医生的可用时段内完成 providerAvailabilityViolation(constraintFactory), // 硬约束:医生从上个就诊地到下一个的驾驶时间必须足够 driveTimeViolation(constraintFactory), // 软约束:优先安排更多就诊(最大化医生的出诊效率) maximizeVisitCount(constraintFactory) }; } // 患者时间窗约束 private Constraint memberTimeWindowViolation(ConstraintFactory constraintFactory) { return constraintFactory.from(Visit.class) .filter(visit -> visit.getActualEndTime().isAfter(visit.getMemberTimeWindowEnd())) .penalize("患者时间窗违规", HardSoftScore.ONE_HARD); } // 医生可用时段约束 private Constraint providerAvailabilityViolation(ConstraintFactory constraintFactory) { return constraintFactory.from(Visit.class) .filter(visit -> visit.getActualEndTime().isAfter(visit.getProvider().getAvailabilityEnd())) .penalize("医生时段违规", HardSoftScore.ONE_HARD); } // 驾驶时间约束 private Constraint driveTimeViolation(ConstraintFactory constraintFactory) { return constraintFactory.from(Visit.class) .join(Visit.class, Joiners.equal(Visit::getProvider), Joiners.equal(Visit::getPreviousVisit)) .filter((nextVisit, prevVisit) -> { Duration requiredDriveTime = calculateDriveTime(prevVisit.getLocation(), nextVisit.getLocation()); return nextVisit.getActualStartTime().isBefore(prevVisit.getActualEndTime().plus(requiredDriveTime)); }) .penalize("驾驶时间不足", HardSoftScore.ONE_HARD); } // 最大化出诊量的软约束 private Constraint maximizeVisitCount(ConstraintFactory constraintFactory) { return constraintFactory.from(Visit.class) .filter(visit -> !(visit instanceof ProviderStartVisit)) // 排除虚拟起始任务 .reward("出诊量最大化", HardSoftScore.ONE_SOFT); } }
三、常见的配置坑&调试技巧
- 别把Provider设为规划实体:Provider是资源,不是需要被规划的任务,把它作为规划值即可,否则会把问题复杂化
- 虚拟起始任务必须加:没有这个锚点,医生的日程时间线就没法正确计算,容易出现时间混乱
- 统一时间单位:所有时间计算都用统一的格式(比如转成分钟数),避免LocalDateTime的时区或格式问题
- 先从简单场景测试:先跑一个医生+一个患者的场景,确认约束生效后再逐步加复杂场景
- 打开调试日志:OptaPlanner的日志会告诉你哪些约束被违反,规划变量的变化过程,方便定位问题
备注:内容来源于stack exchange,提问作者Edmanan Khandelwal
相关产品推荐
相关产品推荐

