使用Java调用Cplex实现了阿里mindopt求解器的案例(https://opt.aliyun.com/platform/case)人员排班问题。
随着现在产业的发展,7*24小时服务的需要,人员排班的问题,逐渐成为了企业管理中的重要环节。人员排班在许多行业都具有广泛的应用价值,主要包括以下几个方面:
总之,人员排班在各行各业都具有重要的实际应用价值,可以帮助企业和机构提高管理效率、降低成本,同时提升员工的工作满意度和整体效能。总之,人员排班在各行各业都具有重要的实际应用价值,可以帮助企业和机构提高管理效率、降低成本,同时提升员工的工作满意度和整体效能。
运筹学中的数学规划方法是计算人员排班问题的一个好方案。人员排班问题在建模时需要考虑多种约束条件,比如:
我们需要考虑企业内各岗位的需求、员工的工作能力以及工作时间的限制等因素。此外,还需关注企业成本与员工满意度的权衡,以确保在合理控制成本的前提下,最大程度地提高员工的工作满意度。属于一个约束复杂,且多目标的问题。在用数学规划方法进行排班时,建议做一些业务逻辑简化问题,否则容易出现问题太大或者不可解的情况。
下面我们将通过一个简单的例子,讲解如何使用数学规划的方法来做人员排班。
个公司有客服岗工作需要安排,不同时间段有不同的用户需求。该公司安排员工上班的班次有三种:早班8-16点、晚班16-24点和夜班0-8点。一周员工最多安排5天上班,最少休息2天。需要保障值班员工能满足需求,且要保障员工休息时间,如前一天安排晚班后,第二天不能安排早班。
请问怎么安排总上班的班次最少,此时的班表是什么样的?
复制代码不能直接运行,需要在IDEA pom.xml中导入阿帕奇读取csv文件的依赖,并且需要导入cplex.jar。
数据可在文章开头阿里mindopt案例地址中获取。
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-csv</artifactId>
<version>1.7</version>
</dependency>
package main.java;
import ilog.concert.*;
import ilog.cplex.IloCplex;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVRecord;
import java.util.logging.Logger;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.stream.IntStream;
public class EmpSchedulingProblem {
public int n_employees;
public int n_days;
public int n_shifts;
int[] days;
int[] shifts;
int[] employees;
int[][] demandOfEmployees;
public static Logger logger = Logger.getLogger("myLogger");
/**
* @param day 某天
* @param shift 某个班次
* @return 某天某班次需求的人数
*/
public int getDemandOfEmployees(int day, int shift) {
return demandOfEmployees[day][shift];
}
public EmpSchedulingProblem() throws IOException {
demandOfEmployees = this.readFile();
employees = IntStream.range(0, n_employees).toArray();
days = IntStream.range(0, n_days).toArray();
shifts = IntStream.range(0, n_shifts).toArray();
}
public int[][] readFile() throws IOException {
this.n_shifts = 0;
try (Reader reader = Files.newBufferedReader(Paths.get("src/main/java/mindoptdemo/班次.csv"))) {
Iterable<CSVRecord> records = CSVFormat.DEFAULT.parse(reader);
records.iterator().next(); // 跳过第一行
for (CSVRecord record : records) {
String shift = (record.get(0)); // 星期1到星期7,索引为0,故-1
n_shifts += 1;
}
} catch (IOException e) {
logger.warning(e.getMessage());
}
// 调度周期:7天,3班倒
this.n_days = (int) Files.lines(Paths.get(new File("src/main/java/mindoptdemo/需求人数.csv").getPath())).count() - 1;
int[][] day_shift_empNum = new int[n_days][n_shifts];
// commons-csv读取csv文件,需要导入依赖
try (Reader reader = Files.newBufferedReader(Paths.get("src/main/java/mindoptdemo/需求人数.csv"))) {
Iterable<CSVRecord> records = CSVFormat.DEFAULT.parse(reader);
records.iterator().next(); // 跳过第一行
for (CSVRecord record : records) {
int day = Integer.parseInt(record.get(0)) - 1; // 星期1到星期7,索引为0,故-1
int morningShiftEmpNum = Integer.parseInt(record.get(1)); // 早班需要员工的数量
int middleShiftEmpNum = Integer.parseInt(record.get(2)); // 中班需要员工的数量
int nightShiftEmpNum = Integer.parseInt(record.get(3)); // 晚班需要员工的数量
//保存至二维数组,某天某班次需要的员工数量
day_shift_empNum[day][0] = morningShiftEmpNum;
day_shift_empNum[day][1] = middleShiftEmpNum;
day_shift_empNum[day][2] = nightShiftEmpNum;
this.n_employees += morningShiftEmpNum + middleShiftEmpNum + nightShiftEmpNum;
}
this.n_employees = (int) Math.ceil((double) (this.n_employees) / 5) + 1;
// System.out.println("预估排班人数:" + n_employees);
logger.info("预估排班人数:" + n_employees);
} catch (IOException e) {
logger.info(e.getMessage());
}
System.out.println(Arrays.deepToString(day_shift_empNum));
return day_shift_empNum;
}
public void cplexSolve() {
try {
// 声明cplex优化模型
IloCplex model = new IloCplex();
// 声明决策变量,x_ijk表示员工i在第j天上班次k
IloIntVar[][][] x = new IloIntVar[n_employees][n_days][n_shifts];
for (int i = 0; i < n_employees; i++) {
for (int j = 0; j < n_days; j++) {
for (int k = 0; k < n_shifts; k++) {
// boolVar()声明x_ijk为0-1变量
x[i][j][k] = model.boolVar();
}
}
}
// 约束:每天各个班次在岗的人数符合需求
for (int d = 0; d < days.length; d++) {
for (int s = 0; s < shifts.length; s++) {
IloLinearIntExpr expr = model.linearIntExpr();
for (int e = 0; e < n_employees; e++) {
// addTerm()表示 1*x_eds
expr.addTerm(1, x[e][d][s]);
}
model.addGe(expr, this.getDemandOfEmployees(d, s));
}
}
// 约束:每人每天最多只有一个班次
for (int n : employees) {
for (int d : days) {
IloLinearIntExpr expr = model.linearIntExpr();
for (int s : shifts) {
expr.addTerm(1, x[n][d][s]);
}
model.addLe(expr, 1);
}
}
// 约束:前一天是晚班的,第二天不能是早班
for (int e : employees) {
for (int d : days) {
IloLinearIntExpr expr = model.linearIntExpr();
// 0 早班
// 1 中班
// 2 晚班
// 当天上晚班的员工,第二天不能上早班
expr.addTerm(1, x[e][d][2]);
if (d == 6) {
expr.addTerm(1, x[e][0][0]);
} else {
expr.addTerm(1, x[e][d + 1][0]);
}
model.addLe(expr, 1);
}
}
// 约束:一周工作工作时间不能超过5天
for (int e = 0; e < n_employees; e++) {
IloLinearIntExpr expr = model.linearIntExpr();
for (int d = 0; d < days.length; d++) {
for (int s = 0; s < shifts.length; s++) {
expr.addTerm(1, x[e][d][s]);
}
}
model.addLe(expr, 5);
}
// 目标:雇佣的员工最少,即有排班的班次总数最少
IloLinearIntExpr expr = model.linearIntExpr();
for (int e : employees) {
for (int d : days) {
for (int s : shifts) {
expr.addTerm(1, x[e][d][s]);
}
}
}
model.addMinimize(expr);
// 打印求解结果
if (model.solve()) {
System.out.println("num of employees: " + n_employees);
System.out.println("solution status: " + model.getStatus());
System.out.println("solution value: " + model.getObjValue());
System.out.printf("%-8s", " ");
for (int d = 0; d < n_days; d++) {
System.out.printf("\t%d", d + 1);
}
System.out.println();
for (int e : employees) {
System.out.printf("employee%d\t", e + 1);
int shiftCount = 0;
for (int d : days) {
int shift = 0;
for (int s : shifts) {
if (((int) model.getValue(x[e][d][s])) != 0) {
shift = s + 1;
shiftCount += 1;
}
}
System.out.printf("%d\t", shift);
}
System.out.printf("员工%d这周上%d个班次", e + 1, shiftCount);
System.out.println();
}
}
model.end();
} catch (IloException e) {
logger.warning(e.getMessage());
}
}
public static void main(String[] args) {
try {
EmpSchedulingProblem esp = new EmpSchedulingProblem();
esp.cplexSolve();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}