在日常开发中,我们经常会遇到这类"棘手"的优化问题:学校要排课程表,得满足"一个老师不能同时上两节课""一个房间不能同时用""学生不喜欢连续上同一门课"等一堆限制;企业要排员工排班,得兼顾员工技能、休息时间和业务需求。这些问题看似简单,实则属于NP-Hard问题------没有万能的快速解法,但只要找到"相对最优解"就能满足业务需求。
OptaPlanner就是为解决这类问题而生的开源Java约束求解器。它内部封装了各种AI优化算法,我们不用深入研究算法细节,只需通过API定义问题和约束,就能让它自动找到最优解决方案。本文就从"是什么、核心概念、算法原理"三个入门要点,再到"课程表规划"完整实战,带你快速掌握OptaPlanner。
一、什么是OptaPlanner?
OptaPlanner是轻量级、可嵌入的Java开源规划引擎(AI约束求解器),核心作用是:在满足一系列约束条件的前提下,对复杂问题找到"相对最优解"。
用大白话解释:就像你请了一个"智能规划助手",你把所有限制条件(比如"老师A周三下午不能上课""房间B只能上理科课")和优化目标(比如"尽量让老师连堂""减少学生换教室的次数")告诉它,它就会在合理时间内给出一份最符合要求的安排,不用你手动一点点试错。
它支持解决的典型场景:
- 资源分配类:员工排班、车辆路径规划、云资源调度;
- 日程安排类:课程表规划、会议安排、设备维护调度;
- 组合优化类:工厂作业调度、装箱问题(比如快递装箱最大化利用空间)。
二、核心概念:用"课程表"例子讲明白
OptaPlanner有几个必须理解的核心概念,直接套"课程表规划"的例子,一看就懂(就像G1的Region用"内存分区"理解一样):
2.1 问题事实(Problem Fact)
求解问题时"固定不变"的基础数据,是规划的前提。就像搭积木的"固定零件",不能改。
课程表场景示例:
- 时间段(Timeslot):比如"周一08:30-09:30""周二14:30-15:30",这些是提前确定好的,求解时不会新增或修改;
- 房间(Room):比如"房间A""房间B",数量和属性固定;
- 老师(Teacher):比如"张老师(教语文)""李老师(教数学)",技能和可用时间固定;
- 班级(StudentGroup):比如"1班""2班",学生数量固定。
2.2 规划实体(Planning Entity)
需要被"优化安排"的对象,是求解过程中核心操作的载体。就像积木里"需要摆放的零件"。
课程表场景示例:课程(Lesson)。我们要给每节课安排"哪个时间段上""在哪个房间上",所以课程就是需要被规划的实体。
2.3 规划变量(Planning Variable)
规划实体上"可以动态修改"的属性,也是找到最优解的关键。就像零件上"可以调整的摆放位置"。
课程表场景示例:课程的时间段(timeslot) 和房间(room)。这两个属性是可变的,调整它们就能得到不同的课程表安排。
2.4 约束(Constraint)
规划问题的"规则和限制",决定了哪些安排是合理的、哪些是不合理的。OptaPlanner把约束分为两类,和我们日常规划逻辑完全一致:
- 硬约束(Hard Constraint):必须严格遵守,一旦违反,这个安排就是"不可行的"。比如"一个房间同一时间只能上一节课""一位老师同一时间只能教一节课"------违反了就会出现"冲突",这样的课程表根本没法用;
- 软约束(Soft Constraint):不是必须遵守,但遵守了会让安排更优。比如"老师更喜欢连堂上课(减少课间换教室的麻烦)""学生不喜欢连续上同一门课""老师尽量在同一个房间上课"------违反了也能用,但体验不好。
2.5 分数(Score)
用来衡量"某个安排好不好"的标准,直接由约束决定。OptaPlanner用"惩罚(违反约束)"和"奖励(符合软约束)"来计算分数:
- 违反硬约束:会受到"硬惩罚",分数直接降低(比如扣1000分),硬分数为负的安排是不可行的;
- 违反软约束:会受到"软惩罚"(比如扣1分);符合软约束:会得到"软奖励"(比如加1分);
- 最终分数越高,安排越优。
举个例子:某课程表违反了"房间冲突"硬约束,硬分数扣1000分(硬分数=-1000);但满足了"老师连堂"软约束,软分数加5分(软分数=5)。这个安排是不可行的,因为硬分数为负。
三、核心算法原理:用"生活化例子"讲透
OptaPlanner内部封装了多种优化算法,不用我们手动实现,理解它们的核心逻辑就能更好地适配场景。这些算法就像"不同的找最优解的思路",下面用大白话+例子解释最常用的4种:
3.1 禁忌搜索(Tabu Search):不重复走"老路"
核心逻辑:找到一个较好的解后,记录下来"不能再走回头路"(比如不能再回到上一个差的安排),避免陷入"局部最优解"。
生活化例子:你要找从家到公司的最优路线(最短时间+最少红绿灯)。第一次试了路线A,觉得不错;接下来不会再重复走路线A,而是尝试路线B、C......如果找到比A更好的路线D,就把D作为新的"基准",再围绕D尝试其他路线。这样就能避免一直停留在"还不错"的路线A,找到更优的路线D。
OptaPlanner中的作用:比如排课程表时,不会反复切换到之前已经验证过的"有冲突的安排",而是持续探索新的、更优的安排。
3.2 模拟退火(Simulated Annealing):允许"暂时不完美",避免错过最优解
核心逻辑:模拟物理中"金属退火"的过程------一开始温度高,分子运动剧烈,允许较大的变化(即使暂时变差);随着温度降低,分子运动平缓,不再接受变差的变化,最终稳定在最优状态。
生活化例子:你找最优路线时,一开始可以尝试"绕点远路"(即使暂时变慢),因为可能绕远路后能找到更短的后续路线;慢慢的,就不再尝试绕远路,只在当前好路线的附近微调。比如一开始你试了路线A(20分钟),然后尝试路线B(25分钟,变差了),但继续走路线B的延伸路线C,发现只要18分钟(更优);到后期,就不会再尝试超过20分钟的路线了。
OptaPlanner中的作用:避免一开始就陷入"局部最优解"。比如排课程表时,一开始可能会接受"有一个软约束违反"的安排,但后续可能会在这个安排的基础上,找到完全符合软约束的最优解。
3.3 遗传算法(Genetic Algorithm):优胜劣汰,"进化"出最优解
核心逻辑:模拟生物进化的"自然选择、交叉、变异"过程。把每个安排当作一个"个体",多个个体组成"种群";通过选择(保留优秀个体)、交叉(组合两个优秀个体的优点生成新个体)、变异(随机微调个体),让种群不断"进化",最终得到最优个体。
生活化例子:你有10个初步的课程表安排(种群),其中3个安排冲突少、符合软约束多(优秀个体);把这3个优秀安排的"优点"组合起来(比如A的上午安排+B的下午安排),生成新的安排;再随机微调新安排的某个课程时间(变异);重复这个过程,最终会得到比原来更优的课程表。
OptaPlanner中的作用:适合大规模优化问题(比如几百个课程、几十个房间的规划),能通过"进化"快速筛选出最优解。
3.4 局部搜索(Local Search):在当前解附近"微调"找更优解
核心逻辑:从一个初始解开始,在它的"邻域"(比如微调一个课程的时间或房间)内寻找更好的解;找到后,以新解为基准继续微调,直到找不到更好的解为止。
生活化例子:你找到一个20分钟的上班路线,然后微调路线(比如换一个红绿灯路口),发现时间变成19分钟;再以19分钟的路线为基准,微调另一个路口,发现18分钟;直到微调后时间不再减少,就确定18分钟是当前最优路线。
OptaPlanner中的作用:适合中小规模问题,求解速度快,能快速找到接近最优的解。
提示:OptaPlanner会自动选择或组合这些算法(默认是"混合算法"),我们不用手动指定,只需配置求解时间(比如"求解3分钟")即可。
四、实战:从零搭建最优课程表

下面以"学校课程表规划"为场景,完整实现从"定义问题"到"运行求解"的全流程。技术栈:Java + Maven + OptaPlanner,最终实现一份无硬约束冲突、尽量符合软约束的课程表。
4.1 第一步:明确需求(约束定义)
先把课程表的约束明确下来,后续代码都围绕这些约束编写:
硬约束(必须遵守):
- 一个房间在同一时间段只能上一节课(无房间冲突);
- 一位老师在同一时间段只能教一节课(无老师冲突);
- 一个班级在同一时间段只能上一节课(无班级冲突)。
软约束(尽量遵守):
- 老师尽量在同一个房间上所有课(减少换教室麻烦);
- 老师尽量连堂上课(减少课间空隙);
- 学生尽量不连续上同一门课(提升学习体验)。
4.2 第二步:环境搭建(Maven依赖)
创建Maven项目,在pom.xml中添加OptaPlanner依赖(最新版本可参考官方文档):
<dependency>
<groupId>org.optaplanner</groupId>
<artifactId>optaplanner-core</artifactId>
<version>9.44.0.Final</version>
</dependency>
<!-- 用于JSON序列化结果(可选,方便展示) -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.15.2</version>
</dependency>
4.3 第三步:建立模型(实体类定义)
根据核心概念,定义4个实体类:问题事实(Timeslot、Room、Teacher、StudentGroup)、规划实体(Lesson)、解决方案(TimeTable)。
3.1 问题事实类:Timeslot(时间段)
固定的时间段,求解时不修改:
// 时间段(问题事实)
public class Timeslot {
// 唯一标识
private Long id;
// 星期(如"MON")
private String dayOfWeek;
// 开始时间(如"08:30")
private String startTime;
// 结束时间(如"09:30")
private String endTime;
// 构造器、getter、setter、toString
public Timeslot(Long id, String dayOfWeek, String startTime, String endTime) {
this.id = id;
this.dayOfWeek = dayOfWeek;
this.startTime = startTime;
this.endTime = endTime;
}
// getter和setter省略(IDE自动生成)
@Override
public String toString() {
return dayOfWeek + " " + startTime + "-" + endTime;
}
}
3.2 问题事实类:Room(房间)
固定的房间,求解时不修改:
// 房间(问题事实)
public class Room {
private Long id;
private String name; // 如"房间A"
// 构造器、getter、setter、toString
public Room(Long id, String name) {
this.id = id;
this.name = name;
}
// getter和setter省略
@Override
public String toString() {
return name;
}
}
3.3 问题事实类:Teacher(老师)和StudentGroup(班级)
简单定义,存储基本信息:
// 老师(问题事实)
public class Teacher {
private Long id;
private String name; // 如"张老师"
public Teacher(Long id, String name) {
this.id = id;
this.name = name;
}
// getter和setter省略
@Override
public String toString() {
return name;
}
}
// 班级(问题事实)
public class StudentGroup {
private Long id;
private String name; // 如"1班"
public StudentGroup(Long id, String name) {
this.id = id;
this.name = name;
}
// getter和setter省略
@Override
public String toString() {
return name;
}
}
3.4 规划实体类:Lesson(课程)
需要优化的实体,标注@PlanningEntity;规划变量(timeslot、room)标注@PlanningVariable:
import org.optaplanner.core.api.domain.entity.PlanningEntity;
import org.optaplanner.core.api.domain.variable.PlanningVariable;
// 课程(规划实体):需要被安排时间段和房间
@PlanningEntity
public class Lesson {
private Long id;
private String subject; // 课程名称(如"语文")
private Teacher teacher; // 授课老师(固定,问题事实)
private StudentGroup studentGroup; // 授课班级(固定,问题事实)
// 规划变量1:时间段(需要OptaPlanner分配)
@PlanningVariable(allowsUnassigned = true) // allowsUnassigned=true:允许初始未分配
private Timeslot timeslot;
// 规划变量2:房间(需要OptaPlanner分配)
@PlanningVariable(allowsUnassigned = true)
private Room room;
// 构造器(无参构造器必须有,OptaPlanner反射需要)
public Lesson() {}
// 带参构造器(初始化固定属性)
public Lesson(Long id, String subject, Teacher teacher, StudentGroup studentGroup) {
this.id = id;
this.subject = subject;
this.teacher = teacher;
this.studentGroup = studentGroup;
}
// getter和setter省略
@Override
public String toString() {
return subject + "(" + teacher + "-" + studentGroup + ") " + timeslot + "-" + room;
}
}
3.5 解决方案类:TimeTable(课程表)
封装所有问题数据和规划结果,标注@PlanningSolution;分数标注@PlanningScore:
import org.optaplanner.core.api.domain.solution.PlanningSolution;
import org.optaplanner.core.api.domain.solution.ProblemFactCollectionProperty;
import org.optaplanner.core.api.domain.solution.PlanningEntityCollectionProperty;
import org.optaplanner.core.api.domain.score.Score;
import org.optaplanner.core.api.domain.score.buildin.hardsoft.HardSoftScore;
import java.util.List;
// 课程表(解决方案):包含所有问题数据和规划结果
@PlanningSolution
public class TimeTable {
// 问题事实集合:所有可用时间段(OptaPlanner会自动识别为规划变量的可选值)
@ProblemFactCollectionProperty
private List<Timeslot> timeslotList;
// 问题事实集合:所有可用房间
@ProblemFactCollectionProperty
private List<Room> roomList;
// 规划实体集合:所有需要安排的课程
@PlanningEntityCollectionProperty
private List<Lesson> lessonList;
// 分数:OptaPlanner计算的结果(硬分数+软分数)
@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;
}
// getter和setter省略
}
4.4 第四步:定义约束(分数计算逻辑)
实现ConstraintProvider接口,定义所有硬约束和软约束的计算逻辑。OptaPlanner通过这些逻辑自动计算每个安排的分数。
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[] {
// 硬约束:1. 房间冲突(同一房间同一时间段不能有两节课)
roomConflict(constraintFactory),
// 硬约束:2. 老师冲突(同一老师同一时间段不能上两节课)
teacherConflict(constraintFactory),
// 硬约束:3. 班级冲突(同一班级同一时间段不能上两节课)
studentGroupConflict(constraintFactory),
// 软约束:1. 老师尽量在同一房间上课(违反则惩罚)
teacherRoomStability(constraintFactory),
// 软约束:2. 老师尽量连堂上课(符合则奖励)
teacherTimeEfficiency(constraintFactory),
// 软约束:3. 学生尽量不连续上同一门课(违反则惩罚)
studentGroupSubjectVariety(constraintFactory)
};
}
// 硬约束1:房间冲突
private Constraint roomConflict(ConstraintFactory constraintFactory) {
// 逻辑:找到两个不同的课程,它们的时间段相同且房间相同 → 违反硬约束,扣1个硬分数
return constraintFactory
.forEachUniquePair(Lesson.class, // 选择两个不同的课程
Joiners.equal(Lesson::getTimeslot), // 条件1:时间段相同
Joiners.equal(Lesson::getRoom) // 条件2:房间相同
)
.penalize("Room conflict", HardSoftScore.ONE_HARD); // 惩罚:扣1硬分
}
// 硬约束2:老师冲突
private Constraint teacherConflict(ConstraintFactory constraintFactory) {
return constraintFactory
.forEachUniquePair(Lesson.class,
Joiners.equal(Lesson::getTimeslot),
Joiners.equal(Lesson::getTeacher) // 条件:同一老师
)
.penalize("Teacher conflict", HardSoftScore.ONE_HARD);
}
// 硬约束3:班级冲突
private Constraint studentGroupConflict(ConstraintFactory constraintFactory) {
return constraintFactory
.forEachUniquePair(Lesson.class,
Joiners.equal(Lesson::getTimeslot),
Joiners.equal(Lesson::getStudentGroup) // 条件:同一班级
)
.penalize("StudentGroup conflict", HardSoftScore.ONE_HARD);
}
// 软约束1:老师尽量在同一房间上课
private Constraint teacherRoomStability(ConstraintFactory constraintFactory) {
// 逻辑:对同一老师的所有课程,统计每个课程的房间与"老师常用房间"是否一致 → 不一致则扣1软分
return constraintFactory
.forEach(Lesson.class)
.groupBy(Lesson::getTeacher, Lesson::getRoom, // 按老师+房间分组
ConstraintCollectors.count()) // 统计每个老师在每个房间的课程数量
.filter((teacher, room, count) -> count <= 1) // 只关注课程数量≤1的房间(即不常用房间)
.penalize("Teacher room stability", HardSoftScore.ONE_SOFT,
(teacher, room, count) -> count); // 惩罚:每个不常用房间的课程扣1软分
}
// 软约束2:老师尽量连堂上课
private Constraint teacherTimeEfficiency(ConstraintFactory constraintFactory) {
// 逻辑:找到同一老师的两节课,它们的时间段连续(如08:30-09:30和09:30-10:30) → 奖励1软分
return constraintFactory
.forEach(Lesson.class)
.join(Lesson.class, // 关联同一老师的另一节课
Joiners.equal(Lesson::getTeacher),
// 条件:两节课的时间段连续(这里简化判断:前一节课的结束时间=后一节课的开始时间)
Joiners.equal(lesson -> lesson.getTimeslot().getEndTime(),
Lesson::getStartTime)
)
.reward("Teacher time efficiency", HardSoftScore.ONE_SOFT); // 奖励:加1软分
}
// 软约束3:学生尽量不连续上同一门课
private Constraint studentGroupSubjectVariety(ConstraintFactory constraintFactory) {
// 逻辑:找到同一班级的两节课,它们的课程相同且时间段连续 → 惩罚1软分
return constraintFactory
.forEach(Lesson.class)
.join(Lesson.class,
Joiners.equal(Lesson::getStudentGroup),
Joiners.equal(Lesson::getSubject), // 条件:同一门课
Joiners.equal(lesson -> lesson.getTimeslot().getEndTime(),
Lesson::getStartTime) // 条件:时间段连续
)
.penalize("StudentGroup subject variety", HardSoftScore.ONE_SOFT);
}
}
4.5 第五步:配置求解器
创建求解器配置文件(src/main/resources/optaplanner/lessonSchedulingSolverConfig.xml),指定约束提供者、求解时间等:
<?xml version="1.0" encoding="UTF-8"?>
<solver xmlns="https://www.optaplanner.org/xsd/solver"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://www.optaplanner.org/xsd/solver https://www.optaplanner.org/xsd/solver/solver.xsd">
<!-- 解决方案类 -->
<solutionClass>com.example.optaplanner.TimeTable</solutionClass>
<!-- 规划实体类 -->
<entityClass>com.example.optaplanner.Lesson</entityClass>
<!-- 约束提供者(分数计算逻辑) -->
<scoreDirectorFactory>
<constraintProviderClass>com.example.optaplanner.TimeTableConstraintProvider</constraintProviderClass>
</scoreDirectorFactory>
<!-- 求解终止条件:最多求解3分钟(可根据需求调整) -->
<termination>
<secondsSpentLimit>180</secondsSpentLimit>
</termination>
</solver>
4.6 第六步:编写主程序,运行求解
初始化问题数据(时间段、房间、老师、班级、课程),创建求解器,运行并输出结果:
import org.optaplanner.core.api.solver.Solver;
import org.optaplanner.core.api.solver.SolverFactory;
import java.util.Arrays;
import java.util.List;
public class LessonSchedulingApp {
public static void main(String[] args) {
// 1. 初始化问题事实:时间段(周一至周二的5个时间段)
List<Timeslot> timeslotList = Arrays.asList(
new Timeslot(1L, "MON", "08:30", "09:30"),
new Timeslot(2L, "MON", "09:30", "10:30"),
new Timeslot(3L, "MON", "10:30", "11:30"),
new Timeslot(4L, "TUE", "08:30", "09:30"),
new Timeslot(5L, "TUE", "09:30", "10:30")
);
// 2. 初始化问题事实:房间(3个房间)
List<Room> roomList = Arrays.asList(
new Room(1L, "房间A"),
new Room(2L, "房间B"),
new Room(3L, "房间C")
);
// 3. 初始化问题事实:老师和班级
Teacher zhangTeacher = new Teacher(1L, "张老师"); // 语文
Teacher liTeacher = new Teacher(2L, "李老师"); // 数学
Teacher zhongTeacher = new Teacher(3L, "钟老师"); // 英语
StudentGroup class1 = new StudentGroup(1L, "1班");
StudentGroup class2 = new StudentGroup(2L, "2班");
StudentGroup class3 = new StudentGroup(3L, "3班");
// 4. 初始化待规划课程(未分配时间段和房间)
List<Lesson> lessonList = Arrays.asList(
new Lesson(1L, "语文", zhangTeacher, class1),
new Lesson(2L, "语文", zhangTeacher, class2),
new Lesson(3L, "数学", liTeacher, class1),
new Lesson(4L, "数学", liTeacher, class3),
new Lesson(5L, "英语", zhongTeacher, class2),
new Lesson(6L, "英语", zhongTeacher, class3)
);
// 5. 构建解决方案(初始状态:课程未分配时间段和房间)
TimeTable unsolvedTimeTable = new TimeTable(timeslotList, roomList, lessonList);
// 6. 创建求解器(加载配置文件)
SolverFactory<TimeTable> solverFactory = SolverFactory.createFromXmlResource(
"optaplanner/lessonSchedulingSolverConfig.xml");
Solver<TimeTable> solver = solverFactory.buildSolver();
// 7. 运行求解器,得到最优解
TimeTable solvedTimeTable = solver.solve(unsolvedTimeTable);
// 8. 输出结果
System.out.println("求解完成!最终分数:" + solvedTimeTable.getScore());
System.out.println("最优课程表安排:");
solvedTimeTable.getLessonList().forEach(lesson ->
System.out.println(lesson.toString()));
}
}
4.7 第七步:运行结果与解读
运行主程序后,输出结果类似如下(无硬约束冲突,软分数较高):
求解完成!最终分数:0hard/3soft
最优课程表安排:
语文(张老师-1班) MON 08:30-09:30-房间A
语文(张老师-2班) MON 09:30-10:30-房间A
数学(李老师-1班) MON 09:30-10:30-房间B
数学(李老师-3班) TUE 08:30-09:30-房间B
英语(钟老师-2班) MON 10:30-11:30-房间A
英语(钟老师-3班) TUE 09:30-10:30-房间A
结果解读:
- 分数:0hard/3soft → 硬分数为0(无硬约束冲突,安排可行),软分数为3(符合3个软约束,比如张老师连堂且在同一房间上课);
- 安排合理:比如张老师的两节课连堂(MON 08:30和09:30)且都在房间A,符合软约束;无房间、老师、班级冲突,符合硬约束。