Opraplanner制服员工轮班计划

Opraplanner uniform employees shifts planning

我在计划员工轮班时遇到问题,因为员工在轮班中均匀(随机)分布。

在我的最小示例中,我使用 Spring boot、Lombock 和 Optaplanner spring boot starter (8.15.0.Final) 包。

我在一个文件中的最小示例:

package com.example.planner;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.optaplanner.core.api.domain.entity.PlanningEntity;
import org.optaplanner.core.api.domain.lookup.PlanningId;
import org.optaplanner.core.api.domain.solution.PlanningEntityCollectionProperty;
import org.optaplanner.core.api.domain.solution.PlanningScore;
import org.optaplanner.core.api.domain.solution.PlanningSolution;
import org.optaplanner.core.api.domain.solution.ProblemFactCollectionProperty;
import org.optaplanner.core.api.domain.valuerange.ValueRangeProvider;
import org.optaplanner.core.api.domain.variable.PlanningVariable;
import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore;
import org.optaplanner.core.api.score.stream.Constraint;
import org.optaplanner.core.api.score.stream.ConstraintFactory;
import org.optaplanner.core.api.score.stream.ConstraintProvider;
import org.optaplanner.core.api.solver.SolverManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import java.util.ArrayList;
import java.util.List;

@SpringBootApplication
public class PlannerApplication implements CommandLineRunner {

    @Autowired
    private SolverManager<Problem, Long> solverManager;

    public static void main(String[] args) {
        SpringApplication.run(PlannerApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        final var problem = new Problem(
                List.of(new Employee(1L), new Employee(2L), new Employee(3L)),
                List.of(new Shift(1L), new Shift(2L), new Shift(3L), new Shift(4L), new Shift(5L), new Shift(6L))
        );
        final var job = solverManager.solveAndListen(1L, id -> problem, bestSolution -> {
            for (final var shift : bestSolution.shifts) {
                System.err.println("Shift " + shift.id + ": Employee " + shift.employee.id);
            }
        });
    }

    @NoArgsConstructor
    public static class PlannerConstraintProvider implements ConstraintProvider {

        @Override
        public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
            return new Constraint[]{};
        }

    }

    @PlanningSolution
    @Data @NoArgsConstructor @AllArgsConstructor
    public static class Problem {
        @ValueRangeProvider(id = "employeeRange")
        @ProblemFactCollectionProperty
        private List<Employee> employees;
        @PlanningEntityCollectionProperty
        private List<Shift> shifts = new ArrayList<>(0);
        @PlanningScore
        private HardSoftScore score;

        public Problem(List<Employee> employees, List<Shift> shifts) {
            this.employees = employees;
            this.shifts = shifts;
        }
    }

    @Data @NoArgsConstructor @AllArgsConstructor
    public static class Employee {
        @PlanningId
        private Long id;
    }

    @PlanningEntity
    @Data @NoArgsConstructor @AllArgsConstructor
    public static class Shift {
        @PlanningId
        private Long id;
        @PlanningVariable(valueRangeProviderRefs = "employeeRange")
        private Employee employee;

        public Shift(Long id) {
            this.id = id;
        }
    }

}

这个例子的输出是:

Shift 1: Employee 1
Shift 2: Employee 1
Shift 3: Employee 1
Shift 4: Employee 1
Shift 5: Employee 1
Shift 6: Employee 1

期望的输出是:

Shift 1: Employee 1
Shift 2: Employee 2
Shift 3: Employee 3
Shift 4: Employee 1
Shift 5: Employee 2
Shift 6: Employee 3

(或其他统一组合)

你没有任何限制。您还没有告诉 OptaPlanner 要优化什么,因此所有解决方案都同样有利于 OptaPlanner。

(其实这段代码没有失败我挺意外的,没有约束的情况应该会抛出异常。)

您还没有定义任何约束,因此OptaPlaner 没有理由提出更好的解决方案。你没有告诉它什么更好。

OptaPlanner“认为”这个解决方案可能是最好的,因为(我猜)它的分数是 0hard/0soft(您可以在控制台中查看),这是一个理想的分数。

要获得所需的输出,您应该定义一个 fair workload distribution 约束条件,该约束条件将对每个员工的工作量进行平方惩罚。可能这样的事情应该适用于你的情况:

        @Override
        public Constraint[] defineConstraints(ConstraintFactory constraintFactory) {
            return new Constraint[] {
                    fairWorkloadDistribution(constraintFactory)
            };
        }

...

Constraint fairWorkloadDistribution(ConstraintFactory constraintFactory) {
    return constraintFactory.forEach(Shift.class)
            .groupBy(Shift::getEmployee, ConstraintCollectors.count())
            .penalize(
                    "Employee workload squared",
                    HardSoftScore.ONE_SOFT,
                    (employee, shifts) -> shifts * shifts);
}