平均分配时间表
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 个惩罚,这完全违背了约束的目标
我在 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 个惩罚,这完全违背了约束的目标