You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多
文档控制台
免费开始使用

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」作为可变属性

每个SlotActivity属性就是我们的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,可以把@PlanningVariablenullable设为true,或者添加一个"无活动"的特殊Activity类型,避免处理null值。
  • 两周的周期可以通过Day对象的属性(比如weekNumber:1或2)来区分,确保统计范围覆盖整个周期。

内容的提问来源于stack exchange,提问作者Jshaps

火山引擎 最新活动