1. OptaPlanner 简介
本教程将介绍一个用于解决约束满足问题的 Java 框架 —— OptaPlanner。
⚠️ 注意:如果你使用的是 OptaPlanner 的 fork 或后续版本,请查看 Timefold Solver 指南。
OptaPlanner 可以通过一套成熟的算法来解决复杂的规划问题,而且配置非常简单。虽然理解这些算法有助于更深入地调优,但框架本身已经替我们完成了大部分复杂工作。
2. Maven 依赖
首先,在项目中添加 OptaPlanner 的 Maven 依赖:
<dependency>
<groupId>org.optaplanner</groupId>
<artifactId>optaplanner-core</artifactId>
<version>8.24.0.Final</version>
</dependency>
你可以从 Maven Central 仓库 获取最新版本。
3. 问题与解决方案类
要使用 OptaPlanner 解决问题,首先需要定义问题模型。我们以课程排课(Lecture Timetabling)为例,因为这类问题涉及多个资源(如教室、时间、教师)的平衡调度,非常适合展示 OptaPlanner 的能力。
3.1. CourseSchedule 类
CourseSchedule
是问题的解决方案类,包含了问题变量和规划实体。
@PlanningSolution
public class CourseSchedule {
@ValueRangeProvider(id = "availableRooms")
@ProblemFactCollectionProperty
private List<Integer> roomList;
@ValueRangeProvider(id = "availablePeriods")
@ProblemFactCollectionProperty
private List<Integer> periodList;
@ProblemFactCollectionProperty
private List<Lecture> lectureList;
@PlanningScore
private HardSoftScore score;
@PlanningSolution
:标记该类为 OptaPlanner 的解决方案类。- OptaPlanner 需要三个基本组件:规划实体(Planning Entity)、问题事实(Problem Facts)和评分(Score)。
3.2. Lecture 类
Lecture
是规划实体类,代表每门课程的安排。
@PlanningEntity
public class Lecture {
@PlanningId
private Long id;
public Integer roomNumber;
public Integer period;
public String teacher;
@PlanningVariable(
valueRangeProviderRefs = {"availablePeriods"})
public Integer getPeriod() {
return period;
}
@PlanningVariable(
valueRangeProviderRefs = {"availableRooms"})
public Integer getRoomNumber() {
return roomNumber;
}
}
在 CourseSchedule
中,我们还需添加:
@PlanningEntityCollectionProperty
public List<Lecture> getLectureList() {
return lectureList;
}
@PlanningEntity
:标记该类为规划实体。@PlanningVariable
:标记可变字段,其值来自@ValueRangeProvider
提供的值集合。
3.3. 问题事实(Problem Facts)
@ValueRangeProvider(id = "availableRooms")
@ProblemFactCollectionProperty
public List<Integer> getRoomList() {
return roomList;
}
@ValueRangeProvider(id = "availablePeriods")
@ProblemFactCollectionProperty
public List<Integer> getPeriodList() {
return periodList;
}
这些是所有可能的教室和时间段,OptaPlanner 将在这些范围内为每个 Lecture 分配值。
评分字段用于记录当前解的质量:
@PlanningScore
public HardSoftScore getScore() {
return score;
}
✅ 没有评分,OptaPlanner 无法判断哪个解是最优的。
4. 评分机制
评分逻辑是问题相关的,因此需要我们自己实现。
4.1. 自定义评分计算
public class ScoreCalculator
implements EasyScoreCalculator<CourseSchedule, HardSoftScore> {
@Override
public HardSoftScore calculateScore(CourseSchedule courseSchedule) {
int hardScore = 0;
int softScore = 0;
Set<String> occupiedRooms = new HashSet<>();
for(Lecture lecture : courseSchedule.getLectureList()) {
String roomInUse = lecture.getPeriod()
.toString() + ":" + lecture.getRoomNumber().toString();
if(occupiedRooms.contains(roomInUse)){
hardScore += -1;
} else {
occupiedRooms.add(roomInUse);
}
}
return HardSoftScore.of(hardScore, softScore);
}
}
📌 这段代码通过 HashSet
检查是否有重复的教室和时间段组合,如果有冲突则扣分。
5. 测试
5.1. 初始化 Solver
SolverFactory<CourseSchedule> solverFactory = SolverFactory.create(new SolverConfig()
.withSolutionClass(CourseSchedule.class)
.withEntityClasses(Lecture.class)
.withEasyScoreCalculatorClass(ScoreCalculator.class)
.withTerminationSpentLimit(Duration.ofSeconds(1)));
solver = solverFactory.buildSolver();
unsolvedCourseSchedule = new CourseSchedule();
5.2. 执行求解并验证
CourseSchedule solvedCourseSchedule = solver.solve(unsolvedCourseSchedule);
assertNotNull(solvedCourseSchedule.getScore());
assertEquals(-4, solvedCourseSchedule.getScore().getHardScore());
📌 示例中我们检查了硬分值,确认是否得到一个“最优”解。
打印优化结果的方法:
public void printCourseSchedule() {
lectureList.stream()
.map(c -> "Lecture in Room "
+ c.getRoomNumber().toString()
+ " during Period " + c.getPeriod().toString())
.forEach(k -> logger.info(k));
}
输出示例:
Lecture in Room 1 during Period 1
Lecture in Room 2 during Period 1
Lecture in Room 1 during Period 2
Lecture in Room 2 during Period 2
Lecture in Room 1 during Period 3
Lecture in Room 2 during Period 3
Lecture in Room 1 during Period 1
Lecture in Room 1 during Period 1
Lecture in Room 1 during Period 1
Lecture in Room 1 during Period 1
⚠️ 最后几条重复了,说明资源不足(3 个时间段、2 个教室,10 个课程),OptaPlanner 已尽力给出一个“相对合理”的解。
6. 更多功能
虽然我们的例子很简单,但 OptaPlanner 支持更多高级特性,例如:
- 自定义优化算法
- 多线程求解(如 Fork/Join、增量求解、多租户)
- Jakarta EE 集成
更多信息请参考官方文档:OptaPlanner 多线程求解
7. 总结
OptaPlanner 是一个强大的约束满足问题求解器,特别适合排课、排班、资源分配等场景。它资源占用小,性能优秀,并被 Red Hat 纳入其业务规则管理系统(BRMS)。
📌 本文示例代码可在 GitHub 仓库 获取。