平均分配时间表

Equal distribution of Schedules

我在 optaplanner 中有一个系统可以为员工生成班次。当有 e.x 6 名员工且班次容量为 2 时,仅针对 2 名员工生成解决方案,直到他们达到最大工作时间。如何添加约束,以便生成包含混合员工的解决方案。

以下是在 optaplanner 中定义的规则:

global HardMediumSoftLongScoreHolder scoreHolder;

// Hard constraints


rule "Required skill for a shift"
    when
        $shift: Shift(employee != null, hasRequiredSkills() == false)
    then
        scoreHolder.penalize(kcontext, $shift.getLengthInMinutes());
end

rule "Unavailable time slot for an employee"
    when
        $availability: EmployeeAvailability(
                state == EmployeeAvailabilityState.UNAVAILABLE,
                $e : employee,
                $startDateTime : startDateTime,
                $endDateTime : endDateTime)
        Shift(employee == $e,
            $startDateTime < endDateTime,
            $endDateTime > startDateTime)
    then
        scoreHolder.penalize(kcontext, $availability.getDuration().toMinutes());
end

rule "No overlapping shifts"
    when
        $s : Shift(employee != null, $e : employee, $firstStartDateTime: startDateTime, $firstEndDateTime : endDateTime)
        $s2: Shift(employee == $e, this != $s,
            $firstStartDateTime < endDateTime,
            $firstEndDateTime > startDateTime)
    then
        scoreHolder.penalize(kcontext, $s2.getLengthInMinutes());
end

rule "No more than 2 consecutive shifts"
    when
        $s : Shift(
                employee != null,
                $e : employee,
                $firstEndDateTime : endDateTime)
        $s2: Shift(
                employee == $e,
                $firstEndDateTime == startDateTime,
                this != $s,
                $secondEndDateTime : endDateTime)
        $s3: Shift(
                employee == $e,
                $secondEndDateTime == startDateTime,
                this != $s,
                this != $s2)
    then
        scoreHolder.penalize(kcontext, $s3.getLengthInMinutes());
end

rule "Break between non-consecutive shifts is at least 10 hours"
    when
        $s : Shift(
                employee != null,
                $e : employee,
                $leftEndDateTime : endDateTime)
        Shift(
                employee == $e,
                $leftEndDateTime < startDateTime,
                $leftEndDateTime.until(startDateTime, ChronoUnit.HOURS) < 10,
                this != $s,
                $rightStartDateTime: startDateTime)
    then
        long breakLength = $leftEndDateTime.until($rightStartDateTime, ChronoUnit.MINUTES);
        scoreHolder.penalize(kcontext, (10 * 60) - breakLength);
end

rule "Daily minutes must not exceed contract maximum"
    when
        $employee : Employee($contract : contract, $contract.getMaximumMinutesPerDay() != null)
        $s : Shift(employee == $employee, $startDateTime : startDateTime)
        accumulate(
            $other : Shift(
                employee == $employee, $shiftStart : startDateTime,
                $shiftEnd : endDateTime,
                $shiftStart.toLocalDate().equals($startDateTime.toLocalDate())
            ),
            $shiftCount : count($other),
            $totalMinutes : sum(Duration.between($shiftStart, $shiftEnd).toMinutes())
        )
        Number(this > $contract.getMaximumMinutesPerDay()) from $totalMinutes
    then
        scoreHolder.penalize(kcontext, (((long)$totalMinutes) - $contract.getMaximumMinutesPerDay()) / $shiftCount);
end

rule "Weekly minutes must not exceed contract maximum"
    when
        $rosterConstraintConfiguration : RosterConstraintConfiguration()
        $employee : Employee($contract : contract, $contract.getMaximumMinutesPerWeek() != null)
        $s : Shift(employee == $employee, $startDateTime : startDateTime)
        accumulate(
            $other : Shift(
                employee == $employee, $shiftStart : startDateTime,
                $shiftEnd : endDateTime,
                DateTimeUtils.sameWeek($rosterConstraintConfiguration.getWeekStartDay(), $shiftStart, $startDateTime)
            ),
            $shiftCount : count($other),
            $totalMinutes : sum(Duration.between($shiftStart, $shiftEnd).toMinutes())
        )
        Number(this > $contract.getMaximumMinutesPerWeek()) from $totalMinutes
    then
        scoreHolder.penalize(kcontext, (((long)$totalMinutes) - $contract.getMaximumMinutesPerWeek()) / $shiftCount);
end

rule "Monthly minutes must not exceed contract maximum"
    when
        $employee : Employee($contract : contract, $contract.getMaximumMinutesPerMonth() != null)
        $s : Shift(employee == $employee, $startDateTime : startDateTime)
        accumulate(
            $other : Shift(
                employee == $employee, $shiftStart : startDateTime,
                $shiftEnd : endDateTime,
                $shiftStart.getMonth() == $startDateTime.getMonth(),
                $shiftStart.getYear() == $startDateTime.getYear()
            ),
            $shiftCount : count($other),
            $totalMinutes : sum(Duration.between($shiftStart, $shiftEnd).toMinutes())
        )
        Number(this > $contract.getMaximumMinutesPerMonth()) from $totalMinutes
    then
        scoreHolder.penalize(kcontext, (((long)$totalMinutes) - $contract.getMaximumMinutesPerMonth()) / $shiftCount);
end

rule "Yearly minutes must not exceed contract maximum"
    when
        $employee : Employee($contract : contract, $contract.getMaximumMinutesPerYear() != null)
        $s : Shift(employee == $employee, $startDateTime : startDateTime)
        accumulate(
            $other : Shift(
                employee == $employee, $shiftStart : startDateTime,
                $shiftEnd : endDateTime,
                $shiftStart.getYear() == $startDateTime.getYear()
            ),
            $shiftCount : count($other),
            $totalMinutes : sum(Duration.between($shiftStart, $shiftEnd).toMinutes())
        )
        Number(this > $contract.getMaximumMinutesPerYear()) from $totalMinutes
    then
        scoreHolder.penalize(kcontext, (((long)$totalMinutes) - $contract.getMaximumMinutesPerYear()) / $shiftCount);
end


// Medium constraints


rule "Assign every shift"
    when
        Shift(employee == null)
    then
        scoreHolder.penalize(kcontext);
end

// Soft constraints


rule "Employee is not original employee"
    when
        $shift: Shift(originalEmployee != null,
                      employee != null,
                      employee != originalEmployee)
    then
        scoreHolder.penalize(kcontext, $shift.getLengthInMinutes());
end

rule "Undesired time slot for an employee"
    when
        $availability: EmployeeAvailability(
                state == EmployeeAvailabilityState.UNDESIRED,
                $e : employee,
                $startDateTime : startDateTime,
                $endDateTime : endDateTime)
        Shift(employee == $e,
                $startDateTime < endDateTime,
                $endDateTime > startDateTime)
    then
        scoreHolder.penalize(kcontext, $availability.getDuration().toMinutes());
end

rule "Desired time slot for an employee"
    when
        $availability: EmployeeAvailability(
                state == EmployeeAvailabilityState.DESIRED,
                $e : employee,
                $startDateTime : startDateTime,
                $endDateTime : endDateTime)
        Shift(employee == $e,
                $startDateTime < endDateTime,
                $endDateTime > startDateTime)
    then
        scoreHolder.reward(kcontext, $availability.getDuration().toMinutes());
end

rule "Employee is not rotation employee"
    when
        $shift: Shift(rotationEmployee != null, employee != null, employee != rotationEmployee)
    then
        scoreHolder.penalize(kcontext, $shift.getLengthInMinutes());
end

您应该在求解器配置文件中添加此配置

<localSearch>
  <unionMoveSelector>
    <changeMoveSelector>
      <selectionOrder>RANDOM</selectionOrder>
    </changeMoveSelector>
  </unionMoveSelector>
</localSearch>

可能是你的时间跨度太短了。假设不是...

您应该制定一个约束,以公平分配工作量。如果这 2 名员工完成了所有工作的 100%,而其他 4 名员工完成了 0%,则存在“公平”问题。

例如,假设要分配给这 6 名员工的总工作量为 12 个工作单元。即使这 12 个单位不超过 yearly/monthly/weekly,甚至不超过每日员工合同的限制或您所做的任何其他限制,那么该额外限制将确保这 12 个工作单位将公平分配给6名员工,理想情况下每人2个工作单元。

现在,如果这 2 名员工获得全部 12 个单位,则约束可能类似于:“对于每个员工,计算他的努力与平均努力的差值的平方,并对结果进行惩罚”。在上面提到的情况下,那就是

(6-2) squared + (6-2) squared + (0-2) squared + (0-2) squared + (0-2) squared + (0-2) squared所以16+16+4+4+4+4=48个惩罚

在理想情况下,您有:

(2-2) squared + (2-2) squared + (2-2) squared + (2-2) squared + (2-2) squared + (2-2) squared 所以 0 个惩罚

如果你不考虑平方,你会:

(6-2) + (6-2) + (0-2) + (0-2) + (0-2) + (0-2) 所以 4+4-2-2-2-2 = 0 个惩罚,这完全违背了约束的目标