You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

能否创建依赖规划变量当前值的动态ValueRangeProvider?

动态ValueRangeProvider依赖规划变量当前值的实现方案

嘿,这个场景我太熟悉了——做排班系统的时候,员工的可用时间范围死死绑定对应班次的要求,这种动态依赖的需求OptaPlanner完全能搞定,而且有几种不同的实现思路,我给你拆解一下:

一、最直接的方案:带上下文参数的@ValueRangeProvider

这是OptaPlanner专门为这类场景设计的特性:你可以让ValueRangeProvider方法接收当前的规划实体作为参数,这样就能直接拿到当前班次的专属时间范围,动态生成员工的可用取值范围。

举个实际代码例子,假设你的规划实体是ShiftAssignment(记录哪个员工分配到哪个班次),里面有固定的shift字段(包含这个班次允许的开始时间窗口),还有要优化的employee规划变量:

@PlanningEntity
public class ShiftAssignment {
    @PlanningVariable(valueRangeProviderRefs = {"employeeStartTimesForShift"})
    private Employee employee;

    // 这个是已经确定的班次,包含该班次独有的开始时间范围
    private Shift shift;

    // getter、setter就不啰嗦了
}

然后在你的规划解决方案类(@PlanningSolution标注的类)里,定义动态的ValueRangeProvider:

@PlanningSolution
public class RosterSolution {
    @ValueRangeProvider(id = "employeeStartTimesForShift")
    public ValueRange<LocalTime> getEmployeeStartTimesForShift(ShiftAssignment shiftAssignment) {
        // 直接拿到当前对应的班次,获取它的专属时间范围
        Shift currentShift = shiftAssignment.getShift();
        // 生成这个班次允许的员工开始时间范围,比如按15分钟间隔拆分
        return ValueRangeFactory.createTimeRange(
            currentShift.getAllowedStartWindowStart(),
            currentShift.getAllowedStartWindowEnd(),
            Duration.ofMinutes(15)
        );
    }

    // 其他必要的属性:规划实体集合、分数计算器之类的,这里就省略了
}

这个方法的核心就是把当前规划实体作为参数传入ValueRangeProvider,直接基于实体的属性(这里就是班次的时间范围)动态生成取值范围,完美匹配你的需求。

二、更复杂场景的替代方案

如果你的需求还要叠加其他条件(比如员工自身的可用时间、已经排过的班次冲突),可以试试这两种方式:

1. 在ValueRangeProvider里做复合筛选

比如你需要先过滤出能适配当前班次时间的员工,再返回他们的可用开始时间:

@ValueRangeProvider(id = "validEmployeeStartTimes")
public ValueRange<LocalTime> getValidEmployeeStartTimes(ShiftAssignment shiftAssignment) {
    Shift currentShift = shiftAssignment.getShift();
    // 先筛选出符合该班次基本时间要求的员工
    List<Employee> eligibleEmployees = findEmployeesWhoCanWorkShift(currentShift);
    // 把这些员工的可用时间和班次的允许范围取交集,再合并成一个统一的取值范围
    return mergeAndIntersectTimeRanges(eligibleEmployees, currentShift.getAllowedStartWindowStart(), currentShift.getAllowedStartWindowEnd());
}

2. 用规划变量的筛选器(PlanningVariableFilter

如果你已经有一个全局的员工集合,但需要过滤掉不符合当前班次时间要求的员工,可以给@PlanningVariable加个筛选器:

@PlanningVariable(valueRangeProviderRefs = {"allEmployees"}, filter = EmployeeShiftTimeMatchFilter.class)
private Employee employee;

然后实现这个筛选器类,判断员工是否适配当前班次的时间:

public class EmployeeShiftTimeMatchFilter implements PlanningVariableFilter<ShiftAssignment, Employee> {
    @Override
    public boolean accept(ShiftAssignment shiftAssignment, Employee employee) {
        Shift currentShift = shiftAssignment.getShift();
        // 检查员工的可用时间和班次的允许开始窗口是否有交集
        return employee.getAvailableStartTime().isBefore(currentShift.getAllowedStartWindowEnd())
                && employee.getAvailableEndTime().isAfter(currentShift.getAllowedStartWindowStart());
    }
}

三、踩过的坑提醒你注意

  • 一定要保证Shift里的时间范围属性是不可变的——规划过程中别修改这些值,不然动态生成的ValueRange会乱掉,导致计算错误。
  • 如果你的排班规模很大(比如几百上千个班次),动态ValueRangeProvider可能会有点性能损耗,建议提前预计算一些常用的时间范围,或者加个简单的缓存优化一下。
  • 如果需要同时依赖多个变量的状态(比如员工之前已经排了别的班次,不能冲突),可以在ValueRangeProvider里注入ScoreDirector,直接获取当前的规划状态来做判断。

内容的提问来源于stack exchange,提问作者Incognito

火山引擎 最新活动