OptaPlanner中学生考试间隔约束失效问题求助
OptaPlanner考试间隔软约束失效问题修复
问题背景
需要实现同一学生的两场考试时间间隔越短,惩罚值越高的软约束:
- 间隔1个时段:惩罚16
- 间隔2个时段:惩罚8
- 间隔3个时段:惩罚4
- 间隔4个时段:惩罚2
- 间隔5个时段:惩罚1
当前代码运行后产生24645个软惩罚,但OptaPlanner完全没有尝试修复,核心问题出在约束逻辑的实现上。
原代码的关键错误
- 共享变量
penalty的线程安全问题:ConstraintProvider是单例,成员变量penalty会被所有约束计算共享,导致惩罚值混乱,无法正确对应每个违规的考试对。 - 约束匹配逻辑错误:
penalize方法中使用的penalty是最后一次filter中设置的值,而非当前匹配项对应的惩罚值,导致所有违规项的惩罚值统一为最后一次计算的结果。 - 低效的全量Join:直接
Student.join(Exam).join(Exam)会产生大量无关匹配,既影响性能,也可能引入错误的约束匹配。
修复后的约束实现代码
public class ExamTableConstraintProvider implements ConstraintProvider { @Override public Constraint[] defineConstraints(ConstraintFactory constraintFactory) { return new Constraint[]{ // Hard constraints twoExamForStudentConflict(constraintFactory), // Soft constraints spaceBetweenExams(constraintFactory) }; } private Constraint twoExamForStudentConflict(ConstraintFactory constraintFactory) { return constraintFactory.forEach(Exam.class) .join(Exam.class, Joiners.equal(Exam::getTimeslot), Joiners.lessThan(Exam::getID)) .filter((exam1, exam2) -> { List<Integer> commonStudents = new ArrayList<>(exam1.getSID()); commonStudents.retainAll(exam2.getSID()); return !commonStudents.isEmpty(); }) .penalize("Student conflict", HardSoftScore.ONE_HARD); } private Constraint spaceBetweenExams(ConstraintFactory constraintFactory) { // 先关联学生和其所有考试 return constraintFactory.forEach(Student.class) .join(Exam.class, Joiners.equal(student -> student.getExamIds(), Exam::getID)) // 再关联同一学生的另一场考试,用lessThan避免重复匹配(如exam1-exam2和exam2-exam1) .join(Exam.class, Joiners.equal((student, exam1) -> student.getExamIds(), Exam::getID), Joiners.lessThan(Exam::getID, (student, exam1) -> exam1.getID())) .filter((student, exam1, exam2) -> { // 仅对间隔≤5的情况进行惩罚 int timeDifference = Math.abs(exam1.getTimeslot().getID() - exam2.getTimeslot().getID()); return timeDifference >= 1 && timeDifference <= 5; }) // 动态计算当前匹配项的惩罚值,避免共享变量 .penalize("Max time between exams", HardSoftScore.ONE_SOFT, (student, exam1, exam2) -> { int timeDifference = Math.abs(exam1.getTimeslot().getID() - exam2.getTimeslot().getID()); return switch (timeDifference) { case 1 -> 16; case 2 -> 8; case 3 -> 4; case 4 -> 2; case 5 -> 1; default -> 0; // 前面filter已经过滤,这里不会触发 }; }); } }
关键改动说明
- 移除共享变量
penalty:在penalize方法中使用lambda表达式,针对每个匹配的(student, exam1, exam2)动态计算惩罚值,确保每个违规项的惩罚值准确对应。 - 优化Join逻辑:
- 用
Joiners.equal(student -> student.getExamIds(), Exam::getID)精准关联学生和其参加的考试,避免全量匹配。 - 用
Joiners.lessThan(Exam::getID, (student, exam1) -> exam1.getID())确保每个考试对只被匹配一次(如exam1 ID < exam2 ID),避免重复惩罚。
- 用
- 精准过滤违规场景:仅对时间间隔1-5的考试对进行惩罚,减少不必要的约束计算。
额外验证建议
- 运行Solver时,开启日志查看约束匹配情况:添加
logging.level.org.optaplanner=DEBUG到配置文件,确认软约束的惩罚值是否正确计算。 - 延长Solver运行时间(如改为
Duration.ofMinutes(5)),观察软惩罚是否逐渐降低,确认Solver正在优化该约束。
内容的提问来源于stack exchange,提问作者BjarnVW
相关产品推荐
相关产品推荐

