python+gurobi求解排班问题
一、问题描述
排班问题(Scheduling Problem),是运筹优化领域的一个经典问题。在日常生活中,企事业单位进行人力资源管理时经常会碰到一个问题,那就是员工的倒班问题。企业每天有额定的在岗人数,如何生成排班方案,使得在不使员工加班的前提下,雇佣更少的员工?
只是模型罢了
我们就以阿米诺斯航空(以下简称A航)在某机场的人力资源规划方案为例:
已知A航在该机场对值机人员的在岗人数需求如表一所示:
表一: 阿米诺斯航空值机人员需求表
Mon. | Tue. | Wed. | Thur. | Fri. | Sat. | Sun. | |
---|---|---|---|---|---|---|---|
06:00-10:00 | 8 | 8 | 8 | 8 | 10 | 10 | 6 |
10:00-14:00 | 12 | 10 | 12 | 10 | 16 | 16 | 8 |
14:00-18:00 | 16 | 12 | 16 | 12 | 20 | 20 | 8 |
18:00-20:00 | 9 | 8 | 9 | 8 | 12 | 12 | 4 |
由表一可见,在每天的不同时段及每周的不同日期,A航对工作人员的在岗数量需求各不相同。A航将每天的营业时间分为四个时段,每个时段4个小时。例如,周一的上午6点至10点需要安排8名员工。航司在排班时还应当考虑如下公司政策及劳务合同的规定(好好好):
- 每个雇员一天内连续工作8小时
- 实行3个轮班工作制,即6:00-14:00, 10:00-18:00, 14:00-22:00
- 每个雇员连续工作5天后休息2天, 7天为一个周期。
如何指定排班方案,使得在不影响企业正常运营的前提下,使企业雇佣的员工最少?
二、模型构建
一周七天,共三个班组,可设置整型变量\(x_{ij}\), 其含义为:
其中,下标变量\(i\), \(j\)的取值分别为:
为使企业雇佣的员工数量最少,定义目标函数如下所示:
随后,我们根据表一中的四个时段分别列出约束条件,这里以周一为例
2.1 时段一:06:00-10:00
在该时段仅有第一班工作人员(6:00-14:00)上岗,其余两班工作人员均未到岗,对照表一可得:
注意\(x_{ij}\)的含义,按照前述的"上五休二"规则.周二和周三上岗的员工此时处于休假期间,故不出现在约束条件中, 同理可列出其余六天的第一时段的约束条件.
2.2 时段二:10:00-14:00
此时段上岗的员工应包括6:00-14:00, 10:00-18:00两个时段, 即:
2.3 时段三:14:00-18:00
此时段上岗的员工应当包括10:00-18:00, 14:00-22:00两个时段, 即:
2.4 时段四:18:00-22:00
在该时段仅有第三班工作人员(6:00-14:00)上岗, 即:
此时我们已经表示出了表一第一列所对应的约束, 同理,对照表一可以得到其余六天的四个时段所对应的约束条件,共计28个不等式约束。
到此,我们已经构建出了完整的排班优化模型。
三、编程思路
gurobipy是gurobi提供的python API, 方便我们在python环境中调用gurobi定义优化模型并求解。
模型拥有28个约束,21个决策变量,为节约时间这里不建议手动输入。我们需要观察一下目标函数和约束条件中,两个下标\(i\), \(j\)取值的分布规律。
3.1 目标函数
目标函数实现逻辑较为简单,双重遍历,快速求和即可,这个在gurobi的API中已经封装了现成的方法:
import gurobipy as grb
m = grb.model("Stuff Scheduling Problem")
# 批量添加决策变量,七行三列表示一周七天,三个班组,参数obj=1表示变量的目标函数系数全是1
# gurobi允许你给模型中的决策变量和约束命名, 这样当模型规模较大时可以按名称查找到对应的约束或变量, 方便调试
week_days = 7
crew_nums = 3
x_mat = m.addVars(week_days, crew_nums, obj=1, vtype = grb.GRB.INTEGER, name = 'x')
以上为gurobi中目标函数的隐式写法,适用于目标函数较简单的情况。我们也可以显式地定义目标函数,代码如下:
import gurobipy as prb
m = grb.model("Stuff Scheduling Problem")
# 此时我们先不指定obj参数的值
week_days = 7
crew_nums = 3
x_mat = m.addVars(week_days, crew_nums, vtype = grb.GRB.INTEGER, name = 'x')
m.setObjective(grb.quicknum(x_mat[i, j] for i in range(week_days) for i in range(crew_nums)), grb.GRB.MINIMIZE)
以上两种写法是等价的,任选其一即可,这里多说一句, 变量x_mat
的索引是由元组(7, 3)来定义的,因此遍历时只能写作x_mat[i, j]
而不能写作x_mat[i][j]
, 否则gurobipy会抛出KeyError。
3.2 约束条件
根据公司政策,员工连续工作5天,休息2天,如表二所示:
表二:阿米诺斯航空值机人员作息日程表
Mon. | Tue. | Wed. | Thur. | Fri. | Sat. | Sun. | |
---|---|---|---|---|---|---|---|
Mon. | 班 | 班 | 班 | 班 | 班 | ||
Tue. | 班 | 班 | 班 | 班 | 班 | ||
Wed. | 班 | 班 | 班 | 班 | 班 | ||
Thur. | 班 | 班 | 班 | 班 | 班 | ||
Fri. | 班 | 班 | 班 | 班 | 班 | ||
Sat. | 班 | 班 | 班 | 班 | 班 | ||
Sun. | 班 | 班 | 班 | 班 | 班 |
表二中,行索引表示日期,列索引表示员工开始工作的日期,如表格第一行第二列的含义为:周一当天,所有周二开始上班的员工此时正在休假。
由于表二的数据分布具有强周期性,我们可以使用如下代码直接生成工作日的索引,无需读取外部数据表:
有无大佬帮忙证明一下这个数学原理是什么
import numpy as np
working_day = np.array([[(i+j) % 7 for j in range(7)] for i in range(7)])
working_day = np.delete(working_day, [1, 2], axis=1)
直接将表一作为输入数据,根据表一中的四个时段分别构造约束,代码如下:
import pandas as pd
req_mat = pd.read_excel(url+"/"+"AminoacScheduling.xlsx").values
m.addConstrs(grb.quicksum(x_mat[j, 0] for j in working_day[i])>=req_mat[0][i] for i in range(week_days))
m.addConstrs(grb.quicksum(x_mat[j, 0] + x_mat[j, 1] for j in working_day[i])>=req_mat[1][i] for i in range(week_days))
m.addConstrs(grb.quicksum(x_mat[j, 1] + x_mat[j, 2] for j in working_day[i])>=req_mat[2][i] for i in range(week_days))
m.addConstrs(grb.quicksum(x_mat[j, 2] for j in working_day[i])>=req_mat[3][i] for i in range(week_days))
至此,模型的目标函数和全部约束条件构建完成,直接求解即可
完整代码
import gurobipy as grb
import numpy as np
import pandas as pd
m = grb.Model("Stuff Scheduling Problem")
# 此时我们先不指定obj参数的值
__week_days__ = 7
__crew_nums__ = 3
__dir__ = r"D:/Coding/ProgramData/AirlineProb"
x_mat = m.addVars(__week_days__, __crew_nums__, vtype=grb.GRB.INTEGER, name='x')
m.setObjective(grb.quicksum(x_mat[i, j] for i in range(__week_days__) for j in range(__crew_nums__)), grb.GRB.MINIMIZE)
working_day = np.array([[(i + j) % __week_days__ for j in range(__week_days__)] for i in range(__week_days__)])
working_day = np.delete(working_day, [1, 2], axis=1)
req_mat = pd.read_excel(__dir__ + "/" + "ManpowerPlan.xlsx").values
m.addConstrs(grb.quicksum(x_mat[k, 0] for k in working_day[i]) >= req_mat[0][i] for i in range(__week_days__))
m.addConstrs(grb.quicksum(x_mat[k, 0] + x_mat[k, 1] for k in working_day[i]) >= req_mat[1][i]
for i in range(__week_days__))
m.addConstrs(grb.quicksum(x_mat[k, 1] + x_mat[k, 2] for k in working_day[i]) >= req_mat[2][i]
for i in range(__week_days__))
m.addConstrs(grb.quicksum(x_mat[k, 2] for k in working_day[i]) >= req_mat[3][i] for i in range(__week_days__))
# m.write("StuffScheduling.lp")
m.optimize()
for v in m.getVars():
# if v.x != 0:
print(v.varName, v.x)