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}\), 其含义为:

\[x_{ij} = 从第i天j班组开始工作的员工数量 \]

其中,下标变量\(i\), \(j\)的取值分别为:

\[\forall i \in \{1,2,3,4,5,6,7\}, \forall j \in \{1,2,3\} \]

为使企业雇佣的员工数量最少,定义目标函数如下所示:

\[\min z = \sum_{i=1}^{7}\sum_{j=1}^{3}x_{ij} \]

随后,我们根据表一中的四个时段分别列出约束条件,这里以周一为例

2.1 时段一:06:00-10:00

在该时段仅有第一班工作人员(6:00-14:00)上岗,其余两班工作人员均未到岗,对照表一可得:

\[x_{11}+x_{41}+x_{51}+x_{61}+x_{71}\geq 8 \]

注意\(x_{ij}\)的含义,按照前述的"上五休二"规则.周二和周三上岗的员工此时处于休假期间,故不出现在约束条件中, 同理可列出其余六天的第一时段的约束条件.

2.2 时段二:10:00-14:00

此时段上岗的员工应包括6:00-14:00, 10:00-18:00两个时段, 即:

\[x_{11}+x_{41}+x_{51}+x_{61}+x_{71}+x_{12}+x_{42}+x_{52}+x_{62}+x_{72}\geq 12 \]

2.3 时段三:14:00-18:00

此时段上岗的员工应当包括10:00-18:00, 14:00-22:00两个时段, 即:

\[x_{12}+x_{42}+x_{52}+x_{62}+x_{72}+x_{13}+x_{43}+x_{53}+x_{63}+x_{73}\geq 16 \]

2.4 时段四:18:00-22:00

在该时段仅有第三班工作人员(6:00-14:00)上岗, 即:

\[x_{11}+x_{41}+x_{51}+x_{61}+x_{71}\geq 9 \]


此时我们已经表示出了表一第一列所对应的约束, 同理,对照表一可以得到其余六天的四个时段所对应的约束条件,共计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)
posted @ 2024-05-18 00:33  KevinScott0582  阅读(320)  评论(0编辑  收藏  举报