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

OptaPlanner求解员工作业调度问题时如何添加业务约束

OptaPlanner员工作业调度实现方案

现有建模问题

当前代码无法实现工时约束的核心原因是建模存在3个硬伤:

  • 只给作业绑定了所属员工,没有规划作业执行顺序。跨作业通行时长完全由先后顺序决定,当前Employee类中通过反向影子变量拿到的jobs列表没有固定顺序,根本算不出准确的总通行时间。这类带点位移动的作业调度本质是带容量约束的车辆路径问题,作业顺序必须纳入规划范围。
  • 字段名不匹配:约束代码里调用的是employee.getApplications(),但Employee类里的承接作业字段实际叫jobs,运行会直接抛方法不存在错误。
  • 成本矩阵缺段:当前Cost仅存储作业点之间的通行时长,没有计算员工从固定出发地(公司/常驻点)到第一个作业点的时间,总工时计算会比实际值偏小。

修正后的领域模型

直接用OptaPlanner原生支持的链式规划变量建模,天然维护作业顺序,性能远好于单独给作业加顺序字段的方案:

  1. 先定义路径节点公共接口,员工和作业都实现这个接口,代表可以作为路径上的停留点:
public interface Standstill {
    Coordinate getCoordinate();
    Job getNextJob();
    Long getLocationId(); // 给每个点位加唯一ID,方便查通行时间缓存
}
  1. 修正Employee类,补全必要字段,作为链式路径的锚点:
@Data
@NoArgsConstructor
@AllArgsConstructor
@PlanningEntity
public class Employee implements Standstill {

    @PlanningId
    private Long id;

    private Coordinate departureCoordinate;

    // 额定工作时长,默认540分钟
    private Integer maxWorkingMinutes = 540;
    // 承接作业数上下限,默认5-10
    private Integer minJobCount = 5;
    private Integer maxJobCount = 10;

    // 链式结构中只需要存路径上的第一个作业,全量作业列表可以通过nextJob递归遍历得到
    @InverseRelationShadowVariable(sourceVariableName = "previousStandstill")
    private Job nextJob;

    @Override
    public Long getLocationId() {
        // 员工点位ID加前缀避免和作业ID重复
        return Long.parseLong("E" + id);
    }

    @Override
    public Coordinate getCoordinate() {
        return departureCoordinate;
    }
}
  1. 修正Job类,改用链式规划变量,加到达时间影子变量缓存累计耗时,避免约束计算时重复遍历路径:
@Data
@NoArgsConstructor
@PlanningEntity
public class Job implements Standstill {

    @PlanningId
    private Long id;

    // 链式规划变量:指向上一个停留节点,可能是员工(路径起点)或其他作业
    @PlanningVariable(valueRangeProviderRefs = {"employeeRange", "jobRange"}, graphType = PlanningVariableGraphType.CHAINED)
    private Standstill previousStandstill;

    @InverseRelationShadowVariable(sourceVariableName = "previousStandstill")
    private Job nextJob;

    private Coordinate coordinate;
    // 单作业固定处理时长30分钟
    private final Integer processTime = 30;

    // 影子变量:缓存到达当前作业点的时间,由自定义变量监听器自动更新
    @ShadowVariable(variableListenerClass = ArrivalTimeUpdatingVariableListener.class, sourceVariableName = "previousStandstill")
    private Long arrivalTime;

    @Override
    public Long getLocationId() {
        return id;
    }
}
  1. 实现时间更新变量监听器,当前序节点变化时自动级联更新后续所有作业的到达时间:
public class ArrivalTimeUpdatingVariableListener implements VariableListener<Plan, Job> {

    @Override
    public void afterEntityAdded(ScoreDirector<Plan> scoreDirector, Job job) {
        updateArrivalTime(scoreDirector, job);
    }

    @Override
    public void afterVariableChanged(ScoreDirector<Plan> scoreDirector, Job job) {
        updateArrivalTime(scoreDirector, job);
    }

    // 其余生命周期方法空实现即可
    @Override public void beforeEntityAdded(ScoreDirector<Plan> scoreDirector, Job job) {}
    @Override public void beforeEntityRemoved(ScoreDirector<Plan> scoreDirector, Job job) {}
    @Override public void afterEntityRemoved(ScoreDirector<Plan> scoreDirector, Job job) {}

    private void updateArrivalTime(ScoreDirector<Plan> scoreDirector, Job job) {
        Standstill previous = job.getPreviousStandstill();
        Long newArrivalTime = null;
        if (previous != null) {
            Long travelTime = getTravelTime(previous.getLocationId(), job.getLocationId(), scoreDirector.getWorkingSolution());
            if (previous instanceof Employee) {
                // 前序是员工起点,到达时间即为从驻地到当前作业的通行时间
                newArrivalTime = travelTime;
            } else if (previous instanceof Job prevJob && prevJob.getArrivalTime() != null) {
                // 前序是其他作业,到达时间=前序作业到达时间+前序作业处理时长+两点通行时间
                newArrivalTime = prevJob.getArrivalTime() + prevJob.getProcessTime() + travelTime;
            }
        }
        // 时间无变化时不更新,避免无效触发分数计算
        if (!Objects.equals(job.getArrivalTime(), newArrivalTime)) {
            scoreDirector.beforeVariableChanged(job, "arrivalTime");
            job.setArrivalTime(newArrivalTime);
            scoreDirector.afterVariableChanged(job, "arrivalTime");
            // 级联更新后续作业的到达时间
            if (job.getNextJob() != null) {
                updateArrivalTime(scoreDirector, job.getNextJob());
            }
        }
    }

    // 从预构建的成本矩阵中查询两点通行时间
    private Long getTravelTime(Long fromId, Long toId, Plan solution) {
        // 提前把GraphHopper计算的所有点位(含员工出发地)的通行时间转成双层Map缓存,key为fromId、toId,直接取值即可
        return solution.getTravelTimeCache().get(fromId).get(toId);
    }
}
  1. 修正Plan类,补全必要的问题事实和值范围:
@Data
@NoArgsConstructor
@PlanningSolution
public class Plan {

    @ProblemFactCollectionProperty
    @ValueRangeProvider(id = "employeeRange")
    private List<Employee> employees;

    @PlanningEntityCollectionProperty
    @ValueRangeProvider(id = "jobRange")
    private List<Job> jobs;

    // 预构建的通行时间缓存,结构为Map<fromLocationId, Map<toLocationId, travelTimeMinutes>>
    @ProblemFactProperty
    private Map<Long, Map<Long, Long>> travelTimeCache;

    @PlanningScore
    private HardSoftScore score;

}

约束实现

修正原有作业数量约束,新增工时超限硬约束:

public class CustomConstraintProvider implements ConstraintProvider {

    @Override
    public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
        return new Constraint[] {
                minJobCountConflict(constraintFactory),
                maxJobCountConflict(constraintFactory),
                workingTimeOverLimit(constraintFactory)
        };
    }

    private Constraint minJobCountConflict(ConstraintFactory constraintFactory) {
        return constraintFactory.forEach(Employee.class)
                .filter(employee -> countJobs(employee) < employee.getMinJobCount())
                .penalize("作业数低于下限", HardSoftScore.ONE_HARD,
                        employee -> employee.getMinJobCount() - countJobs(employee));
    }

    private Constraint maxJobCountConflict(ConstraintFactory constraintFactory) {
        return constraintFactory.forEach(Employee.class)
                .filter(employee -> countJobs(employee) > employee.getMaxJobCount())
                .penalize("作业数超过上限", HardSoftScore.ONE_HARD,
                        employee -> countJobs(employee) - employee.getMaxJobCount());
    }

    private Constraint workingTimeOverLimit(ConstraintFactory constraintFactory) {
        return constraintFactory.forEach(Employee.class)
                .filter(employee -> {
                    Job lastJob = getLastJob(employee);
                    if (lastJob == null || lastJob.getArrivalTime() == null) {
                        return false;
                    }
                    // 总工时=最后一个作业的到达时间+最后一个作业的处理时长
                    long totalTime = lastJob.getArrivalTime() + lastJob.getProcessTime();
                    return totalTime > employee.getMaxWorkingMinutes();
                })
                .penalize("工时超限", HardSoftScore.ONE_HARD, employee -> {
                    Job lastJob = getLastJob(employee);
                    long totalTime = lastJob.getArrivalTime() + lastJob.getProcessTime();
                    return (int) (totalTime - employee.getMaxWorkingMinutes());
                });
    }

    // 遍历链式结构统计员工承接的总作业数
    private int countJobs(Employee employee) {
        int count = 0;
        Job current = employee.getNextJob();
        while (current != null) {
            count++;
            current = current.getNextJob();
        }
        return count;
    }

    // 找到员工路径上的最后一个作业
    private Job getLastJob(Employee employee) {
        Job current = employee.getNextJob();
        if (current == null) {
            return null;
        }
        while (current.getNextJob() != null) {
            current = current.getNextJob();
        }
        return current;
    }
}

注意事项

  • 构建通行时间缓存时,要把所有员工的出发地点位也加入计算,否则无法查询员工到第一个作业点的通行时间。
  • 如果业务要求员工完成所有作业后需要返回出发地,总工时计算要额外加上最后一个作业返回员工出发地的通行时间。
  • 后续可以根据业务需求加软约束,比如最小化总通勤时长、优先分配近距离作业等,进一步优化方案合理性。

内容的提问来源于stack exchange,提问作者Scott A. Levinson

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.27 14:27:17