You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

OptaPlanner:按学生-单科目分组并单独执行penalize的实现问题

问题描述

我的PlanningEntity定义如下:

@PlanningEntity
public class Participation {
    @PlanningId
    private long id;

    private Student student;
    private Lesson lesson;

    @PlanningVariable(valueRangeProviderRefs = "possibleEnrollments")
    private Boolean enrolled;
}

其中Lesson包含方法public List<Subject> getSubjects()(支持多科目关联)。

我需要实现一个约束:按学生+单科目的组合分组所有Participation,比如课程A关联科目1、课程B关联科目2、课程C关联科目1和2时,要得到两个(Student, List<Participation>)分组:

  • 科目1:包含课程A和C的Participation
  • 科目2:包含课程B和C的Participation

但直接用以下方式无法实现需求:

constraintFactory
                .forEach(Participation.class)
                .groupBy(Participation::getStudent, Participation::getSubjects, toList())

因为它会按Participation::getSubjects返回的整个List分组,导致关联多科目的课程C会被单独分成一组,不符合预期。

目前我通过自定义UniConstraintCollector实现了部分功能:

return constraintFactory
        .forEach(Participation.class)
        .groupBy(Participation::getStudent, new UniConstraintCollector<Participation, Map<Subject, List<Participation>>, Map<Subject, List<Participation>>>() {
                    @Override
                    public Supplier<Map<Subject, List<Participation>>> supplier() {
                        return HashMap::new;
                    }

                    @Override
                    public BiFunction<Map<Subject, List<Participation>>, Participation, Runnable> accumulator() {
                        return (map, participation) -> {
                            for(Subject s : participation.getSubjects()) {
                                if(!map.containsKey(s)) {
                                    map.put(s, new ArrayList<>());
                                }
                                map.get(s).add(participation);
                            }

                            return () -> {
                                for(Map.Entry<Subject, List<Participation>> entry: map.entrySet()) {
                                    entry.getValue().remove(participation);
                                }
                            };
                        };
                    }

                    @Override
                    public Function<Map<Subject, List<Participation>>, Map<Subject, List<Participation>>> finisher() {
                        return Function.identity();
                    }
        }).penalize(("name", HardSoftScore.ONE_SOFT, (student, participationMap) -> {
            ...
        });)

但这个方案只能对所有科目-列表组合统一计算惩罚值,我希望对每个科目-列表组合单独执行penalize,有没有实现方法?

另外,我尝试从Subject角度实现,将Subject设为PlanningSolution的ProblemFact,使用如下代码:

return constraintFactory
        .forEach(Subject.class)
        .join(constraintFactory.forEach(Participation.class),
                JoinerSupport.getJoinerService().newBiJoiner(List::of, JoinerType.INTERSECTING, Participation::getSubjects)
        )

但使用JoinerType.INTERSECTING时,会在AbstractLeftHandSide中抛出"Unsupported Joiner Type"异常。

解决方案

方法一:自定义Collector输出扁平化分组项

改写UniConstraintCollector,让它直接输出(Student, Subject, List<Participation>)的扁平化流,而非Map,这样就能对每个科目-学生组合单独处理:

public class StudentSubjectParticipationCollector implements UniConstraintCollector<Participation, Map<Student, Map<Subject, List<Participation>>>, Stream<Tuple3<Student, Subject, List<Participation>>>> {

    @Override
    public Supplier<Map<Student, Map<Subject, List<Participation>>>> supplier() {
        return HashMap::new;
    }

    @Override
    public BiFunction<Map<Student, Map<Subject, List<Participation>>>, Participation, Runnable> accumulator() {
        return (studentMap, participation) -> {
            Student student = participation.getStudent();
            studentMap.computeIfAbsent(student, k -> new HashMap<>());
            Map<Subject, List<Participation>> subjectMap = studentMap.get(student);

            List<Subject> subjects = participation.getSubjects();
            subjects.forEach(subject -> {
                subjectMap.computeIfAbsent(subject, k -> new ArrayList<>()).add(participation);
            });

            // 撤销操作
            return () -> {
                subjects.forEach(subject -> {
                    subjectMap.get(subject).remove(participation);
                    if (subjectMap.get(subject).isEmpty()) {
                        subjectMap.remove(subject);
                    }
                });
                if (subjectMap.isEmpty()) {
                    studentMap.remove(student);
                }
            };
        };
    }

    @Override
    public Function<Map<Student, Map<Subject, List<Participation>>>, Stream<Tuple3<Student, Subject, List<Participation>>>> finisher() {
        return studentMap -> studentMap.entrySet().stream()
                .flatMap(studentEntry -> studentEntry.getValue().entrySet().stream()
                        .map(subjectEntry -> Tuple3.of(studentEntry.getKey(), subjectEntry.getKey(), subjectEntry.getValue())));
    }

    // 静态工厂方法方便调用
    public static StudentSubjectParticipationCollector toStudentSubjectParticipations() {
        return new StudentSubjectParticipationCollector();
    }
}

使用时通过flatMap展开每个分组,再单独执行penalize:

constraintFactory.forEach(Participation.class)
        .groupBy(toStudentSubjectParticipations())
        .flatMap(Function.identity())
        .penalize("每个学生-科目组合单独惩罚", HardSoftScore.ONE_SOFT,
                (tuple) -> {
                    Student student = tuple.getFirst();
                    Subject subject = tuple.getSecond();
                    List<Participation> participations = tuple.getThird();
                    // 针对当前学生-科目组合计算惩罚值
                    return calculatePenalty(participations);
                });

方法二:拆分Participation的科目关联(推荐)

如果业务允许,在问题构建阶段将关联多科目的Participation拆分为多个单科目实例(比如课程C关联科目1和2,就拆成两个Participation,分别关联科目1和科目2)。这样就能直接用标准groupBy实现需求:

constraintFactory.forEach(Participation.class)
        .groupBy(Participation::getStudent, Participation::getSubject, toList())
        .penalize("学生-科目组合惩罚", HardSoftScore.ONE_SOFT,
                (student, subject, participations) -> calculatePenalty(participations));

这种方式避免了自定义Collector的复杂度,规则更直观,也更符合OptaPlanner的约束设计习惯。

关于INTERSECTING JoinerType的问题

OptaPlanner的JoinerType.INTERSECTING目前不支持集合与集合的关联匹配,所以会抛出不支持的异常。如果要从Subject出发构建约束,可以改用filter方式:

constraintFactory.forEach(Subject.class)
        .join(Participation.class)
        .filter((subject, participation) -> participation.getSubjects().contains(subject))
        .groupBy((subject, participation) -> participation.getStudent(),
                 (subject, participation) -> subject,
                 toList((subject, participation) -> participation))
        .penalize("学生-科目组合惩罚", HardSoftScore.ONE_SOFT,
                (student, subject, participations) -> calculatePenalty(participations));

但这种方式性能略低于提前拆分Participation的方案,因为每次都要执行contains检查。

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.18 05:01:06