zoukankan      html  css  js  c++  java
  • 初学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,示例如下:

    时间表的类图

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

    • 时间太赶了, 边学边用,原理还不清楚,目前算是会调用这个黑盒了!
    • 英文文档,看着有点艰难,不过还好吧.
  • 相关阅读:
    JAVA地址通过百度地图API转化为经纬度
    JS验证手机号
    JAVA获取六位随机数
    JS判断是否为“YYYYMMDD”式的日期
    JAVA 通过url下载图片保存到本地
    JAVA汉字转拼音
    JS判断是否是苹果系统(ios)和安卓系统(Android)客户端
    JAVA获取请求链接中所有参数(GET请求)
    如何高效学习 Kubernetes 知识图谱?
    当云原生遇到混合云:如何实现“求变”与“求稳”的平衡
  • 原文地址:https://www.cnblogs.com/zhazhaacmer/p/13896764.html
Copyright © 2011-2022 走看看