Spring Boot Java quick start
01. 排班目标
作出一个简单的课程表timetable,示例如下:
时间表的类图
02. Opta的常用注解说明, 关键实体类说明
@PlanningEntity
use it, OptaPlanner knows that this class changes during solving because it contains one or more planning variables.
@PlanningEntity类下的@PlanningVariable
作用,标明具体的排班变量,示例课程类
@Data
@NoArgsConstructor
@PlanningEntity // so OptaPlanner knows that this class changes during solving because it contains one or more planning variables.
public class Lesson {
/**
* 前4个属于固定输入
*/
private Long id;
private String subject;
private String teacher;
private String studentGroup;
/**
* 这两个对应排班变量 会一直变动
* Refs 引用
*/
@PlanningVariable(valueRangeProviderRefs = "timeslotRange")
private Timeslot timeslot;
@PlanningVariable(valueRangeProviderRefs = "roomRange")
private Room room;
public Lesson(Long id, String subject, String teacher, String studentGroup) {
this.id = id;
this.subject = subject;
this.teacher = teacher;
this.studentGroup = studentGroup;
}
@Override
public String toString() {
return subject + "(" + id + ")";
}
}
两个输入数据类:教师类+课时槽类, Room+Timeslot
@Data
public class Room {
private String name;
}
@Data
public class Timeslot {
private String dayOfWeek;
private LocalTime startTime;
private LocalTime endTime;
}
@PlanningSolution
use it, OptaPlanner knows that this class contains all of the input and output data.
@PlanningSolution下的@ValueRangeProvider
作为输入数据注入到 @PlanningVariable(valueRangeProviderRefs = "xxx")的注解下
@PlanningSolution下的@ProblemFactCollectionProperty
标明输入数据乐行
@PlanningSolution下的@PlanningEntityCollectionProperty
标明输出数据类型
@PlanningSolution下的@PlanningScore
输出评分质量: for example, 0hard/-5soft (硬约束扣0分, 软约束扣了5分)
课程表输入输出数据 实体
@Data
@PlanningSolution // so OptaPlanner knows that this class contains all of the input and output data.
public class TimeTable {
@ValueRangeProvider(id = "timeslotRange") // 对应 @PlanningVariable下的id
@ProblemFactCollectionProperty // 输入 不变
private List<Timeslot> timeslotList; // A timeslotList field with all time slots
@ValueRangeProvider(id = "roomRange") // 对应 @PlanningVariable下的id
@ProblemFactCollectionProperty // 输入 不变
private List<Room> roomList; // 存储所有的Room枚举情况
/**
* 输入时:
* 课程信息 subject, teacher and studentGroup 需要填入;
* timeslot and room fields 为空, timeslot and room fields 正是需要计算的.
*
* 输出时:
* 输出结果存储在在Lesson的timeslot and room fields
*/
@PlanningEntityCollectionProperty // 输出 结果域 (在计算过程中会一直进行尝试,直到尝试到最优解)
private List<Lesson> lessonList;
/**
* 输出评分质量: for example, 0hard/-5soft (硬约束扣0分, 软约束扣了5分)
*/
@PlanningScore
private HardSoftScore score;
private TimeTable() {
}
public TimeTable(List<Timeslot> timeslotList, List<Room> roomList,
List<Lesson> lessonList) {
this.timeslotList = timeslotList;
this.roomList = roomList;
this.lessonList = lessonList;
}
}
03. 约束/打分实体类
普通For循环写法:
/**
* @description 课程表 简单扣分的计算器
* @Date 2020/10/28 18:29
*/
public class TimeTableEasyScoreCalculator implements EasyScoreCalculator<TimeTable, HardSoftScore> {
@Override
public HardSoftScore calculateScore(TimeTable timeTable) {
int hardScore = 0;
for (Lesson a : timeTable.getLessonList()) {
for (Lesson b : timeTable.getLessonList()) {
if (a == b) {
continue;
}
// 双层,不重复,遍历
// 硬约束: 在相同的timeslot里
if (a.getId() < b.getId() && a.getTimeslot() != null && a.getTimeslot().equals(b.getTimeslot())) {
// 一间教室最多只能容纳一堂课
if(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);
}
}
类似Java8的Stream流的写法, 官网写着可以降低时间复杂度:
/**
* @description 课程表 约束 生产者
* The ConstraintProvider scales an order of magnitude better than the EasyScoreCalculator: O(n) instead of O(n²).
* @Date 2020/10/29 10:29
*/
public class TimeTableConstraintProvider implements ConstraintProvider {
@Override
public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
return new Constraint[]{
// Hard constraints
roomConflict(constraintFactory),
teacherConflict(constraintFactory),
studentGroupConflict(constraintFactory),
// Soft constraints are only implemented in the "complete" implementation
};
}
private Constraint roomConflict(ConstraintFactory constraintFactory) {
// 实现的就是TimeTableEasyScoreCalculator的: 一间教室同时只能容纳一节课
return constraintFactory.from(Lesson.class)
.join(Lesson.class,
Joiners.equal(Lesson::getTimeslot),
Joiners.equal(Lesson::getRoom),
Joiners.lessThan(Lesson::getId))
// 加权重
.penalize("Room conflict", HardSoftScore.ONE_HARD);
}
private Constraint studentGroupConflict(ConstraintFactory constraintFactory) {
// 一个学生可以在同一时间 只能教授同一门课
return constraintFactory
.from(Lesson.class)
.join(Lesson.class,
Joiners.equal(Lesson::getTimeslot),
Joiners.equal(Lesson::getStudentGroup),
Joiners.lessThan(Lesson::getId))
// penalize 惩罚
.penalize("Stu conflict", HardSoftScore.ONE_HARD);
}
private Constraint teacherConflict(ConstraintFactory constraintFactory) {
// 一个教室可以在同一时间 只能上一门课
return constraintFactory
.from(Lesson.class)
.join(Lesson.class,
Joiners.equal(Lesson::getTimeslot),
Joiners.equal(Lesson::getTeacher),
Joiners.lessThan(Lesson::getId))
.penalize("Teacher conflict", HardSoftScore.ONE_HARD);
}
}
04. 测试类 (模拟输入数据)
/**
* 记得保持在和启动类的统一目录下
*/
@SpringBootTest
class OptaplannerApplicationTests {
@Resource
private SolverManager<TimeTable, UUID> solverManager;
/**
* 01 课程表测试
*/
@Test
void timeTableTest() {
String data = "{"timeslotList":[{"dayOfWeek":"MONDAY","startTime":"08:30:00","endTime":"09:30:00"},{"dayOfWeek":"MONDAY","startTime":"09:30:00","endTime":"10:30:00"}],"roomList":[{"name":"Room A"},{"name":"Room B"}],"lessonList":[{"id":1,"subject":"Math","teacher":"A. Turing","studentGroup":"9th grade"},{"id":2,"subject":"Chemistry","teacher":"M. Curie","studentGroup":"9th grade"},{"id":3,"subject":"French","teacher":"M. Curie","studentGroup":"10th grade"},{"id":4,"subject":"History","teacher":"I. Jones","studentGroup":"10th grade"}]}";
TimeTable problem = JSON.parseObject(data, TimeTable.class);
UUID problemId = UUID.randomUUID();
// Submit the problem to start solving
SolverJob<TimeTable, UUID> solverJob = solverManager.solve(problemId, problem);
TimeTable solution;
try {
// Wait until the solving ends
solution = solverJob.getFinalBestSolution();
} catch (InterruptedException | ExecutionException e) {
throw new IllegalStateException("Solving failed.", e);
}
System.out.println(solution);
}
}
05. 测试输出课程表
{
"timeslotList": [ # 输入数据: 两个上课时间段
{
"dayOfWeek": "MONDAY",
"startTime": "08:30:00",
"endTime": "09:30:00"
},
{
"dayOfWeek": "MONDAY",
"startTime": "09:30:00",
"endTime": "10:30:00"
}
],
"roomList": [ # 输入数据: 两间教室
{
"name": "Room A"
},
{
"name": "Room B"
}
],
"lessonList": [ # 输出结果
{
"id": 1,
"subject": "Math",
"teacher": "A. Turing",
"studentGroup": "9th grade",
"timeslot": { # 排班结果
"dayOfWeek": "MONDAY",
"startTime": "08:30:00",
"endTime": "09:30:00"
},
"room": { # 排班结果
"name": "Room A"
}
},
{
"id": 2,
"subject": "Chemistry",
"teacher": "M. Curie",
"studentGroup": "9th grade",
"timeslot": { # 排班结果
"dayOfWeek": "MONDAY",
"startTime": "09:30:00",
"endTime": "10:30:00"
},
"room": { # 排班结果
"name": "Room A"
}
},
{
"id": 3,
"subject": "French",
"teacher": "M. Curie",
"studentGroup": "10th grade",
"timeslot": { # 排班结果
"dayOfWeek": "MONDAY",
"startTime": "08:30:00",
"endTime": "09:30:00"
},
"room": { # 排班结果
"name": "Room B"
}
},
{
"id": 4,
"subject": "History",
"teacher": "I. Jones",
"studentGroup": "10th grade",
"timeslot": { # 排班结果
"dayOfWeek": "MONDAY",
"startTime": "09:30:00",
"endTime": "10:30:00"
},
"room": { # 排班结果
"name": "Room B"
}
}
],
"score": "0hard/0soft"
}
06. maven依赖
https://docs.optaplanner.org/7.45.0.Final/optaplanner-docs/html_single/index.html#_what_youll_need
07. 最后
- 时间太赶了, 边学边用,原理还不清楚,目前算是会调用这个黑盒了!
- 英文文档,看着有点艰难,不过还好吧.