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

基于Timefold处理模板班次重复调度的约束优化问题

问题背景与需求

我现有MasterSchedule、LiveSchedule、TemplateShift、Shift和Employee这几个领域模型,但当前Timefold代码仅用到Shift和Employee,规划变量为员工与班次的分配。MasterSchedule与TemplateShift支持用户修改未来规划班次并设置生效日期,最终会将MasterSchedule同步至LiveSchedule。

TemplateShift无具体日期,仅存储周期周数、周期内的星期几、起止时间,生成Shift时会基于当前周转换为具体日期(比如第1周第2天对应当前周周二),班次也支持跨天(例如周一20:00至周二6:00)。

我需要同时优化TemplateSchedule与LiveSchedule,希望通过模板生成Shift传入Timefold简化处理,但遇到模板班次的跨周期冲突问题:比如1周周期的MasterSchedule中,第1天1:00-5:00的模板班次与第7天22:00-3:00的班次可能冲突(后者跨到第8天,即下一周的第1天),需要将第8天视为第1天来校验约束并惩罚冲突,求最优解决方案。

现有领域模型代码

@PlanningEntity
public class Shift {
  @PlanningId
  Long id;

  Location location;
  LocalDateTime start;
  LocalDateTime end;
  boolean locked;

  @PlanningVariable(nullable = true)
  Employee employee;

  @PlanningPin
  public boolean isLocked() {
    return locked;
  }
}


public class Employee {
  @PlanningId
  Long id;
  String name;
  double rate;
}

解决方案

核心思路

将TemplateShift的周期特性转化为Timefold可处理的循环时间窗口,通过计算班次在周期内的相对时间区间,在约束规则中处理跨周期的冲突逻辑;同时保留模板转实体的生成逻辑,兼顾TemplateSchedule和LiveSchedule的优化需求。

步骤1:扩展TemplateShift模型,增加周期与相对时间计算

给TemplateShift补充周期属性,并实现相对时间、实际时间的转换方法,方便后续约束校验与Shift生成:

public class TemplateShift {
    @PlanningId
    Long id;
    Integer cycleWeeks; // 周期周数,默认1表示每周重复
    Integer dayOfCycle; // 周期内的第几天(1=周一,7=周日)
    LocalTime startTime;
    LocalTime endTime;
    boolean locked;
    Location location;

    // 计算模板班次在指定周期偏移下的实际开始时间
    public LocalDateTime getStartDateTime(LocalDate baseDate, int cycleOffset) {
        // 以周一为周期起始基准
        LocalDate cycleBase = baseDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
        LocalDate shiftDate = cycleBase.plusDays(dayOfCycle - 1).plusWeeks(cycleOffset);
        return LocalDateTime.of(shiftDate, startTime);
    }

    // 计算模板班次在指定周期偏移下的实际结束时间,自动处理跨天
    public LocalDateTime getEndDateTime(LocalDate baseDate, int cycleOffset) {
        LocalDate cycleBase = baseDate.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY));
        LocalDate shiftDate = cycleBase.plusDays(dayOfCycle - 1).plusWeeks(cycleOffset);
        LocalDateTime end = LocalDateTime.of(shiftDate, endTime);
        // 若结束时间早于开始时间,说明跨天,自动加1天
        if (end.isBefore(getStartDateTime(baseDate, cycleOffset))) {
            end = end.plusDays(1);
        }
        return end;
    }

    // 获取班次在周期内的相对起始分钟数(从周期第一天0点开始计算)
    public int getRelativeStartMinutes() {
        return (dayOfCycle - 1) * 24 * 60 + startTime.getHour() * 60 + startTime.getMinute();
    }

    // 获取班次在周期内的相对结束分钟数,跨天则追加一天的分钟数
    public int getRelativeEndMinutes() {
        int baseMinutes = (dayOfCycle - 1) * 24 * 60 + endTime.getHour() * 60 + endTime.getMinute();
        if (endTime.isBefore(startTime)) {
            baseMinutes += 24 * 60;
        }
        return baseMinutes;
    }
}

步骤2:实现跨周期冲突的约束规则

在Timefold约束流中,基于TemplateShift的相对时间区间,校验同周期内的班次冲突(包括跨周期的重叠情况):

public class TemplateShiftConstraints {
    private static final int MINUTES_PER_WEEK = 7 * 24 * 60;

    public Constraint overlappingTemplateShifts(ConstraintFactory constraintFactory) {
        return constraintFactory.forEach(TemplateShift.class)
                .join(TemplateShift.class,
                        Joiners.equal(TemplateShift::getCycleWeeks), // 仅校验同周期的班次
                        Joiners.lessThan(TemplateShift::getPlanningId), // 避免重复校验同一对班次
                        Joiners.filtering(this::isOverlappingInCycle))
                .penalize("跨周期重叠模板班次", HardSoftScore.ONE_HARD)
                .asConstraint();
    }

    // 判断两个模板班次在周期内是否重叠(含跨周期情况)
    private boolean isOverlappingInCycle(TemplateShift s1, TemplateShift s2) {
        int cycleTotalMinutes = s1.getCycleWeeks() * MINUTES_PER_WEEK;
        int s1Start = s1.getRelativeStartMinutes();
        int s1End = s1.getRelativeEndMinutes();
        int s2Start = s2.getRelativeStartMinutes();
        int s2End = s2.getRelativeEndMinutes();

        boolean s1Wraps = s1End > cycleTotalMinutes;
        boolean s2Wraps = s2End > cycleTotalMinutes;

        if (s1Wraps || s2Wraps) {
            // 跨周期班次拆分为两个区间:[start, 周期结束] 和 [0, 溢出部分],分别校验重叠
            return (isIntervalOverlap(s1Start, cycleTotalMinutes, s2Start, s2End) ||
                    isIntervalOverlap(0, s1End - cycleTotalMinutes, s2Start, s2End)) ||
                    (isIntervalOverlap(s2Start, cycleTotalMinutes, s1Start, s1End) ||
                    isIntervalOverlap(0, s2End - cycleTotalMinutes, s1Start, s1End));
        } else {
            // 普通区间重叠校验
            return isIntervalOverlap(s1Start, s1End, s2Start, s2End);
        }
    }

    // 基础区间重叠判断逻辑
    private boolean isIntervalOverlap(int aStart, int aEnd, int bStart, int bEnd) {
        return aStart < bEnd && bStart < aEnd;
    }
}

步骤3:TemplateShift到Shift的生成逻辑

当优化LiveSchedule时,根据模板生成指定时间范围内的Shift实例,传入Timefold进行员工分配:

public class ShiftGenerator {
    public List<Shift> generateShiftsFromTemplate(List<TemplateShift> templateShifts, LocalDate startDate, int weeksToGenerate) {
        List<Shift> shifts = new ArrayList<>();
        for (TemplateShift template : templateShifts) {
            for (int offset = 0; offset < weeksToGenerate; offset++) {
                LocalDateTime shiftStart = template.getStartDateTime(startDate, offset);
                LocalDateTime shiftEnd = template.getEndDateTime(startDate, offset);
                Shift shift = new Shift();
                shift.setId(template.getId() * 1000 + offset); // 生成唯一Shift ID
                shift.setLocation(template.getLocation());
                shift.setStart(shiftStart);
                shift.setEnd(shiftEnd);
                shift.setLocked(template.isLocked());
                shifts.add(shift);
            }
        }
        return shifts;
    }
}

步骤4:双Schedule优化策略

  • TemplateSchedule优化:直接将TemplateShift作为规划实体,规划变量可设为Employee(提前绑定固定员工到模板)或调整班次的相对时间(允许修改模板时间),用上述跨周期约束处理冲突。
  • LiveSchedule优化:用ShiftGenerator生成未来N周的Shift实例,结合原有员工分配逻辑,通过@PlanningPin注解保留Locked班次的固定分配。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 11:33:18