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

如何在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 09:20:56