2.2.6 定义约束并计算得分
*score(分数)*表示特定解决方案的质量,越高越好。OptaPlanner通过在可用时间寻找最高得分的解决方案的方式来寻找最优方案,它也可能是最佳方案。
由于此用例具有硬约束和软约束, 可以使用HardSoftScore类来表示分数:
- 不能打破硬约束。例如:一个房间最多可以同时上一节课。
- 不应打破软约束。例如:教师更喜欢在相同的房间里教学。
硬约束与其他硬约束进行加权。 软约束也会与其他软约束进行加权。无论每种约束的权重如何,硬约束的权重总是超过软约束。
可以通过实现EasyScoreCalculator类来计算分数:
代码语言:javascript复制public class TimeTableEasyScoreCalculator implements EasyScoreCalculator<TimeTable, HardSoftScore> {
// 入参是解决方案类实例,为每个解决方案计算其得分
@Override
public HardSoftScore calculateScore(TimeTable timeTable) {
List<Lesson> lessonList = timeTable.getLessonList();
int hardScore = 0;
// 比较所有的课程
for (Lesson a : lessonList) {
for (Lesson b : lessonList) {
// 比较相同时间段内的两个课程
if (a.getTimeslot() != null && a.getTimeslot().equals(b.getTimeslot())
&& a.getId() < b.getId()) {
// 在一个时间段,不同的课程必须分配在不同的房间内
if (a.getRoom() != null && a.getRoom().equals(b.getRoom())) {
hardScore--;
}
// 在一个时间段内,不同的课程必须由不同的教师授课
if (a.getTeacher().equals(b.getTeacher())) {
hardScore--;
}
// 在一个时间段内,一个学生只能参加一门课
if (a.getStudentGroup().equals(b.getStudentGroup())) {
hardScore--;
}
}
}
}
// 不考虑软约束
int softScore = 0;
return HardSoftScore.of(hardScore, softScore);
}
}
不幸的是,这不能很好地扩展,因为它是非增量的:每次将一节课分配到不同的时间段或房间时,都需要重新评估所有课程以计算新分数。 作为替代,可以实现一个ConstraintProvider类来执行增量分数计算:
代码语言:javascript复制package org.acme.schooltimetabling.solver;
import org.acme.schooltimetabling.domain.Lesson;
import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore;
import org.optaplanner.core.api.score.stream.Constraint;
import org.optaplanner.core.api.score.stream.ConstraintFactory;
import org.optaplanner.core.api.score.stream.ConstraintProvider;
import org.optaplanner.core.api.score.stream.Joiners;
public class TimeTableConstraintProvider implements ConstraintProvider {
// 定义约束
@Override
public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
return new Constraint[] {
// 硬约束
roomConflict(constraintFactory),
teacherConflict(constraintFactory),
studentGroupConflict(constraintFactory),
};
}
// 定义约束逻辑的思路近似sql查询,从每一个规划方案的规划实体数据集中查询出符合条件的数据予以评分
private Constraint roomConflict(ConstraintFactory constraintFactory) {
// 在一个时间段,不同的课程必须分配在不同的房间内
// 选择一个课程...
return constraintFactory
.forEach(Lesson.class)
// ...与另一个课程进行关联比较...
.join(Lesson.class,
// ...在相同的时间段...
Joiners.equal(Lesson::getTimeslot),
// ...在相同的房间...
Joiners.equal(Lesson::getRoom),
// ...关联的两个课程是不同的实例(拥有不同的id,并且不进行反向关联比较) ...
Joiners.lessThan(Lesson::getId))
// ...对于每一对满足以上关联条件的课程,都使用一个硬约束权重来进行处罚(负分)
.penalize(HardSoftScore.ONE_HARD)
.asConstraint("Room conflict");
}
private Constraint teacherConflict(ConstraintFactory constraintFactory) {
// 在一个时间段内,不同的课程必须由不同的教师授课
return constraintFactory.forEach(Lesson.class)
.join(Lesson.class,
Joiners.equal(Lesson::getTimeslot),
Joiners.equal(Lesson::getTeacher),
Joiners.lessThan(Lesson::getId))
.penalize(HardSoftScore.ONE_HARD)
.asConstraint("Teacher conflict");
}
private Constraint studentGroupConflict(ConstraintFactory constraintFactory) {
// 在一个时间段内,一个学生只能参加一门课
return constraintFactory.forEach(Lesson.class)
.join(Lesson.class,
Joiners.equal(Lesson::getTimeslot),
Joiners.equal(Lesson::getStudentGroup),
Joiners.lessThan(Lesson::getId))
.penalize(HardSoftScore.ONE_HARD)
.asConstraint("Student group conflict");
}
}
ConstraintProvider的复杂度是O(n),EasyScoreCalculator的复杂度是O(n²),可以大幅度改善复杂度。
2.2.7. 在规划方案中收集领域对象
创建TimeTable类包装一个数据集中所有的Timeslot,Room和Lesson实例。此外,由于它包含所有课程,每个课程都包含特定的规划变量状态,所以TimeTable就是一个规划方案,并且它包含对应的分数:
- 如果课程尚未分配,那么它是一个uninitialized solution(未初始化方案),例如得分是 -4init/0hard/0soft 的方案。
- 如果破坏了硬约束,那么它是一个infeasible solution(不可行方案),例如得分是 -2hard/-3soft 的方案。
- 如果遵守 了所有硬约束,那么他是一个* feasible solution(可行方案)*,例如得分是 0hard/-7soft 的方案。
import org.optaplanner.core.api.domain.solution.PlanningEntityCollectionProperty;
import org.optaplanner.core.api.domain.solution.PlanningScore;
import org.optaplanner.core.api.domain.solution.PlanningSolution;
import org.optaplanner.core.api.domain.solution.ProblemFactCollectionProperty;
import org.optaplanner.core.api.domain.valuerange.ValueRangeProvider;
import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore;
// 规划方案类
@PlanningSolution
public class TimeTable {
// 值域提供器
@ValueRangeProvider
// 问题事实集合属性(求解过程中不会改变)
@ProblemFactCollectionProperty
private List<Timeslot> timeslotList;
@ValueRangeProvider
@ProblemFactCollectionProperty
private List<Room> roomList;
// 规划实体集合属性(求解过程中会改变)
// 对于每一个Lesson集合实例,timeslot和room这些规划变量字段通常是空值,其他subject、teacher和studentGroup这些问题属性字段需要赋值
@PlanningEntityCollectionProperty
private List<Lesson> lessonList;
// 规划得分
@PlanningScore
private HardSoftScore score;
public TimeTable() {
}
public TimeTable(List<Timeslot> timeslotList, List<Room> roomList, List<Lesson> lessonList) {
this.timeslotList = timeslotList;
this.roomList = roomList;
this.lessonList = lessonList;
}
...
}
这个类作为方案输出时:
- lessonList字段中的每一个Lesson实例的timeslot和room字段都会在求解后被赋予非空值
- score会被赋予表示这个输出方案的质量的值,例如 0hard/-5soft
2.2.7.1. 值域提供者
timeslotList字段是一个值域提供者。它保存了可用于给Lesson实例的timeslot字段赋值的所有Timeslot实例。 timeslotList字段具有**@ValueRangeProvider**注解,通过匹配规划变量的类型与值域提供者的类型,可以连接对应的@PlanningVariable。
2.2.7.2. 问题事实和规划实体属性
此外,OptaPlanner 还需要知道它可以更改哪些 Lesson 实例以及如何通过 TimeTableConstraintProvider 获取用于计算得分的 Timeslot 和 Room 实例。 timeslotList 和 roomList 字段具有 @ProblemFactCollectionProperty 注解,因此 TimeTableConstraintProvider 可以从这些实例中选择。 lessonList 具有 @PlanningEntityCollectionProperty 注解,因此 OptaPlanner 可以在求解过程中更改它们,而 TimeTableConstraintProvider 也可以从这些实例中选择。