如何在Timefold/Optaplanner中强制同系列学生组课程同时间段同教室?
优化OptaPlanner同组同类型课程排课约束的实现
问题背景
需求:强制同系列学生组的指定类型(SEMINAR)课程安排在同一Timeslot和Room。
原实现代码存在以下问题:
- 扩展性差:通过遍历所有课程对的方式,当课程数量增多时,性能会显著下降。
- 重复惩罚:当一组中有N门课违反约束时,会产生N*(N-1)/2次惩罚(例如10门课里1门违规,得分会扣9分而非1分),导致分数计算失真。
- 尝试使用
ifNotExists()方法时,遇到Unchecked generics array creation for varargs parameter的泛型警告。
原代码:
Constraint seminarsGroupedInTheSameTimeslot(ConstraintFactory constraintFactory){ return constraintFactory //select each 2 pair of different lessons .forEachUniquePair(Lesson.class, //with the same group Joiners.equal((lesson) -> lesson.getStudentGroup().getGroup()), //with the same series Joiners.equal((lesson) -> lesson.getStudentGroup().getName()), //with the same subject Joiners.equal(Lesson::getSubject), //with the same Seminar type Joiners.equal((lesson) -> lesson.getType().equals(LessonType.SEMINAR)) ) //check if the lessons have different timeslots and rooms .filter(((lesson, lesson2) -> { return !(lesson.getTimeslot().equals(lesson2.getTimeslot()) && lesson.getRoom().equals(lesson2.getRoom())); })) .penalize(HardSoftScore.ONE_HARD) .asConstraint("Seminars should be grouped in the same timeslot and room"); }
优化方案
方案一:使用forEachGroup按组聚合约束(推荐)
该方案直接按业务维度分组,仅对违规组惩罚一次,性能和扩展性最优。
Constraint seminarsGroupedInSameTimeslotAndRoom(ConstraintFactory constraintFactory) { return constraintFactory // 按学生组系列、组、科目、课程类型分组,仅筛选SEMINAR类型 .forEachGroup(Lesson.class, Joiners.equal(lesson -> lesson.getStudentGroup().getName()), // 同系列 Joiners.equal(lesson -> lesson.getStudentGroup().getGroup()), // 同组 Joiners.equal(Lesson::getSubject), // 同科目 Joiners.equal(Lesson::getType, LessonType.SEMINAR) // 指定SEMINAR类型 ) // 检查组内是否存在与基准课程(组内第一个)时间或教室不一致的课程 .filter((studentSeries, studentGroup, subject, type, lessonList) -> { if (lessonList.size() <= 1) { return false; // 单门课无约束问题 } Lesson referenceLesson = lessonList.get(0); return lessonList.stream() .anyMatch(lesson -> !lesson.getTimeslot().equals(referenceLesson.getTimeslot()) || !lesson.getRoom().equals(referenceLesson.getRoom())); }) // 每个违规组仅惩罚一次,避免重复扣分 .penalize(HardSoftScore.ONE_HARD) .asConstraint("Seminars of same group-series-subject must share timeslot and room"); }
核心优势:
- 性能高效:无需遍历所有课程对,直接按组聚合处理,数据量越大优势越明显。
- 无重复惩罚:每个违规组仅触发一次惩罚,分数计算准确。
- 扩展性强:后续调整分组维度(如新增约束条件),只需修改
forEachGroup的Joiners参数即可。
方案二:修复ifExists()的泛型警告问题
若偏好基于课程对的检查方式,可通过压制安全的泛型警告+ID过滤避免重复惩罚。
@SuppressWarnings("unchecked") // 明确压制泛型警告,此处类型匹配安全 Constraint seminarsGroupedWithIfExists(ConstraintFactory constraintFactory) { return constraintFactory .forEach(Lesson.class) .filter(lesson -> lesson.getType() == LessonType.SEMINAR) // 检查当前课程是否存在同组、同系列、同科目但时间/教室不同的课程 .ifExists(Lesson.class, Joiners.equal(lesson -> lesson.getStudentGroup().getName()), Joiners.equal(lesson -> lesson.getStudentGroup().getGroup()), Joiners.equal(Lesson::getSubject), Joiners.equal(Lesson::getType, LessonType.SEMINAR), Joiners.notEqual(Lesson::getId), // 排除当前课程本身 Joiners.or( Joiners.notEqual(Lesson::getTimeslot), Joiners.notEqual(Lesson::getRoom) ) ) // 仅让组内ID较小的课程触发惩罚,避免同一冲突对被重复计算 .filter((lesson, conflictingLesson) -> lesson.getId() < conflictingLesson.getId()) .penalize(HardSoftScore.ONE_HARD) .asConstraint("Seminars must share timeslot and room with group peers"); }
说明:
@SuppressWarnings("unchecked")用于压制泛型数组创建警告,因为所有Joiners的类型均与Lesson.class匹配,属于安全操作。- 通过
lesson.getId() < conflictingLesson.getId()确保每个冲突对仅被惩罚一次,解决重复扣分问题。
内容的提问来源于stack exchange,提问作者Diallo Francis Patrick
相关产品推荐
相关产品推荐

