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 仓库 获取。


原始标题:A Guide to OptaPlanner