You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.15 11:54:51