初学OptaPlanner-02- 基于Spring Boot实现一个简单课程表排班的实例 Spring Boot Java quick start

学习链接:

https://docs.optaplanner.org/7.45.0.Final/optaplanner-docs/html_single/index.html#springBootJavaQuickStart

01. 排班目标

作出一个简单的课程表timetable,示例如下:

初学OptaPlanner-02- 基于Spring Boot实现一个简单课程表排班的实例
Spring Boot Java quick start

时间表的类图

初学OptaPlanner-02- 基于Spring Boot实现一个简单课程表排班的实例
Spring Boot Java quick start

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. 最后

  • 时间太赶了, 边学边用,原理还不清楚,目前算是会调用这个黑盒了!
  • 英文文档,看着有点艰难,不过还好吧.