时间折叠:当初始解决方案已经可行时,“构建”解决方案

问题描述 投票:0回答:2

我有一个称为

@PlanningEntity
Participation
,其中
@PlanningVariable
enrolled
指示
Student
是否正在参加相关的
Appointment

@PlanningEntity
public class Participation {
    private Student student;
    private Appointment appointment;

    @PlanningVariable
    private Boolean enrolled = false;
}

在我的具体用例中,最佳解决方案通常是大多数(但不是全部)

Participation
都参加的解决方案。事实上,如果我将
PlanningVariable
初始化为
null
而不是
false
并从
FIRST_FIT
构建阶段开始,我会获得大约 3-4 倍的性能提升,以找到相同的解决方案(或在至少)。我想是因为大多数
enrolled
值设置为
true
LocalSearchPhase
期间可能更有效。

支持我的

PlanningSolution
的数据模型目前无法区分未参加的
Participation
(
enrolled = false
) 和尚未做出的选择 (
enrolled = null
)。因此,尽管这是一个可能的解决方案,但理想情况下,我希望通过使用假的
PlanningEntity
值初始化我的
enrolled
(预先将其设置为
null
)来防止更改基础数据。

我尝试过使用自定义 Forager 配置进行实验,例如使用

.withPickEarlyType(LocalSearchPickEarlyType.FIRST_BEST_SCORE_IMPROVING)
并实现自定义
SelectionFilter
,仅选择具有
enrolled==false
的实体,但这似乎没有太大区别。

那么,问题是:是否可以在当前解决方案已初始化的解决方案上进行类似

FIRST_FIT
的构造?我基本上想告诉 Timefold 对每个实体只查看一次,并贪婪地设置
enrolled=true
(如果这会带来更好的解决方案分数)。

编辑:另一种尝试是实现

CustomPhaseCommand
作为我的求解器的第一阶段。直观上这似乎是一种合乎逻辑的方法,但是我不知道如何检查更改对我的解决方案分数的影响。如果我实现
changeWorkingSolution
将所有
enrolled
值设置为
true
,则不会执行该阶段,因为将 every 值设置为 true 不会提高解决方案分数。有没有办法从
CustomPhaseCommand
内部检查改进?

编辑2:请参阅下面我的“答案”

optaplanner timefold
2个回答
0
投票

我通过

CustomPhaseCommand
找到了一个完全按照预期工作的解决方案。我已经执行了如下命令:

    class InitialConstructionPhase implements CustomPhaseCommand<Timetable> {
        @Override
        public void changeWorkingSolution(ScoreDirector<Timetable> scoreDirector) {
            InnerScoreDirector<Timetable, HardSoftScore> innerScoreDirector = (InnerScoreDirector<Timetable, HardSoftScore>) scoreDirector;

            scoreDirector.getWorkingSolution().getParticipations().stream().filter(participation -> {
                return !participation.getEnrolled();
            }).forEach(participation -> {
                HardSoftScore oldScore = innerScoreDirector.calculateScore();
                
                setParticipationEnrollment(scoreDirector, participation, true);

                HardSoftScore newScore = innerScoreDirector.calculateScore();

                if(oldScore.compareTo(newScore) > 0) {
                    setParticipationEnrollment(scoreDirector, participation, false);
                }
            });
        }
        
        private void setParticipationEnrollment(ScoreDirector<Timetable> scoreDirector, Participation participation, boolean enrolled) {
            scoreDirector.beforeVariableChanged(participation, "enrolled");
            participation.setEnrolled(enrolled);
            scoreDirector.afterVariableChanged(participation, "enrolled");
            scoreDirector.triggerVariableListeners();
        }
    }

我在

https://github.com/TimefoldAI/timefold-solver/blob/7bb9cc2b67c51e88f1ef72bfa940b5df0de9bb91/core/core-impl/src/test/java/ai/timefold/solver/core/config中找到了
InnerScoreDirector的案例/solver/EnvironmentModeTest.java#L276C13-L277C77 。这是实现此目标的推荐方法吗?像这样对
InnerScoreDirector
进行强制转换感觉很奇怪,但我没有看到另一种方法来计算问题分数在CustomPhaseCommand期间

值得注意的是,我看到 在 Optaplanner 7.29.0.Final 中,

calculateScore
方法可在
ScoreDirector
上使用,但我猜这已被删除?


0
投票

我不完全确定为什么需要求解器来执行此操作。引用你的话:

那么,问题是:是否可以在当前解决方案已初始化的解决方案上进行类似 FIRST_FIT 的构造?我基本上想告诉 Timefold 对每个实体只查看一次,如果这会带来更好的解决方案分数,则贪婪地设置 enrolled=true 。

这对于本地搜索或约束求解器来说并不是真正的问题。这是一个简单的循环,它修改解决方案并计算每一步的分数。我建议您查看

SolutionManager#update(Solution)
,并在循环中使用它。

您将失去增量计算的所有性能优势。如果需要非常快地完成此操作,可以通过

SolutionManager
执行与
InnerScoreDirector
相同的操作。请注意,
InnerScoreDirector
和相关 API 是私有的,不能保证稳定。

© www.soinside.com 2019 - 2024. All rights reserved.