Optaplanner活动调度建模:规划实体与规划变量选择咨询
OptaPlanner 建模建议:Group-时段-活动调度问题
嘿,我来帮你理清楚这个OptaPlanner的建模问题~ 你的场景是典型的资源分配类调度问题,我们可以一步步拆解核心实体和变量的选择:
核心概念对应
先明确OptaPlanner里的两个关键角色:
- Planning Entity:需要做出决策的「单元」——也就是那些需要被分配值的对象
- Planning Variable:每个Planning Entity上可以动态调整的属性,也就是我们要分配的内容
你的场景下的选择
1. Planning Entity:用「Slot(时段槽)」作为核心规划实体
你提到的Group、Day、Period的组合,其实就是一个需要被分配Activity的「空位」,我习惯叫它Slot(或者GroupDayPeriod)。每个Slot代表某个Group在某一天的某个时段,这正是我们需要决策的最小单元——因为每个这样的单元都要选一个Activity。
为什么不直接用Group作为Planning Entity?因为Group的决策不是单一的,而是分散在每一天的每个时段,每个时段都是独立的决策点,把每个决策点单独作为Planning Entity更合理。
2. Planning Variable:用「Activity」作为可变属性
每个Slot的Activity属性就是我们的Planning Variable——OptaPlanner会帮我们给每个Slot分配合适的Activity,满足你设定的数量约束。
其他对象的定位
你提到的Group、Day、Period、Activity都属于Problem Fact(问题事实):它们是固定的输入数据,不会在规划过程中被修改,用来定义约束和计算分数。比如:
- Group里可以存储该Group对每种Activity的两周要求数量(比如一个
Map<Activity, Integer>) - Day用来标识两周内的具体日期
- Period区分每天的3个时段
- Activity是所有可选的活动类型
代码示例参考
Planning Entity 定义
@PlanningEntity public class Slot { // 这些都是Problem Fact,初始化后固定不变 private final Group group; private final Day day; private final Period period; // Planning Variable:需要OptaPlanner分配的Activity @PlanningVariable(valueRangeProviderRefs = {"activityRange"}, nullable = false) private Activity activity; // 构造器、Getter、Setter(注意Problem Fact属性建议用final,避免意外修改) public Slot(Group group, Day day, Period period) { this.group = group; this.day = day; this.period = period; } // Getters... public Group getGroup() { return group; } public Day getDay() { return day; } public Period getPeriod() { return period; } public Activity getActivity() { return activity; } public void setActivity(Activity activity) { this.activity = activity; } }
Problem Fact:Group 示例
public class Group { private final String id; // 存储该Group对每种Activity的两周要求数量 private final Map<Activity, Integer> requiredActivityCounts; public Group(String id, Map<Activity, Integer> requiredActivityCounts) { this.id = id; this.requiredActivityCounts = requiredActivityCounts; } // Getters... public String getId() { return id; } public int getRequiredCountForActivity(Activity activity) { return requiredActivityCounts.getOrDefault(activity, 0); } }
约束实现(Activity数量要求)
用OptaPlanner的Constraint Stream来实现硬约束,确保每个Group的Activity数量符合要求:
public class ScheduleConstraintProvider implements ConstraintProvider { @Override public Constraint[] defineConstraints(ConstraintFactory constraintFactory) { return new Constraint[] { enforceRequiredActivityCounts(constraintFactory) }; } private Constraint enforceRequiredActivityCounts(ConstraintFactory constraintFactory) { return constraintFactory.from(Slot.class) // 按Group和Activity分组,统计每个组合的数量 .groupBy(Slot::getGroup, Slot::getActivity, count()) // 关联对应的Group,获取要求数量 .join(Group.class, equal((groupActivityCount) -> groupActivityCount.getKey1(), group -> group)) // 过滤出数量不匹配的情况 .filter((groupActivityCount, group) -> { Activity activity = groupActivityCount.getKey2(); int actualCount = groupActivityCount.getCount(); int requiredCount = group.getRequiredCountForActivity(activity); return actualCount != requiredCount; }) // 惩罚不匹配的情况,差值越大惩罚越重 .penalize("Activity Count Mismatch", HardSoftScore.ONE_HARD, (groupActivityCount, group) -> { Activity activity = groupActivityCount.getKey2(); int actualCount = groupActivityCount.getCount(); int requiredCount = group.getRequiredCountForActivity(activity); return Math.abs(actualCount - requiredCount); }); } }
额外注意点
- 在你的Solution类里,需要用
@ValueRangeProvider提供所有可选的Activity,比如:@PlanningSolution public class ScheduleSolution { @ValueRangeProvider(id = "activityRange") private List<Activity> allActivities; @PlanningEntityCollectionProperty private List<Slot> allSlots; // 分数属性、其他Problem Facts... } - 如果允许某些时段不安排Activity,可以把
@PlanningVariable的nullable设为true,或者添加一个"无活动"的特殊Activity类型,避免处理null值。 - 两周的周期可以通过Day对象的属性(比如
weekNumber:1或2)来区分,确保统计范围覆盖整个周期。
内容的提问来源于stack exchange,提问作者Jshaps




