python+gurobi求解排班問題

KevinScott0582發表於2024-05-18

一、問題描述

排班問題(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)

相關文章