内容介绍
因工作需求,需要在公交发车时刻表和行程时间已经确定的情况下,完成公交车型排班和司机排班的建模,并使用Google的ortools求解器进行求解。
场景描述
某市某一条公交线路,根据客流情况已经完成双向发车时刻表的编排,并统计完成单向行程时间。考虑到该线路的实际运营情况,考虑使用三种车型:
车型 | 特点 |
---|---|
上下午班 | 需要两个司机 |
全天班 | 需要1.5个司机 |
高峰班 | 需要一个司机 |
其中高峰班仅在早晚高峰期间运行。考虑到实际情况,一般一个司机只跟一辆车绑定,特殊情况下(如全天班)一个司机可与两辆车绑定。
要求在当下发车时刻表的前提下,并要求每个司机的工作时间在8小时左右,完成公交车型车次排班,要求车辆数最少、司机数目最少,并保证司机到站的休息时间。
抽象目标以及约束
目标:
- 车辆数最少
- 司机数目最少
约束:
- 每个发车时刻仅有一辆车发车
- 每辆车不能在同一车站连续发车
- 每辆车发车后,在到达终点站前不能再次安排发车
- 每辆车的运营时间在一定范围内,从而保证司机的工作时间
- 每辆车的始发站是浙大站
建立数学模型
模型参数说明
-
λ i j \lambda_{ij} λij表示是否安排第 i 类(上述3类)车中的第 j 辆加入排班,如果使用则取1,否则取0
-
α \alpha α为权重系数
-
x k i j x_{kij} xkij 车辆发车排班0-1矩阵。k表示车辆类型(1为上下午班,2为全天班,3为高峰班),i为发车班次(按照发车时刻表),j为某类型车辆的编号
例如: x 162 x_{162} x162表示上下班车中编号为2的车在时刻6发车 -
y i j y_{ij} yij 车站发车时刻表矩阵。
y[:,0]表示发车站点编号(-1为A站,1为B站)
y[:,1]表示发车时刻
y[:,2]表示行程时间
y[:,3]表示将发车时刻转为距离00:00的分钟时间戳;比如发车时刻为10:00,可转为600
y[:,4]表示表示将到达时刻转为距离00:00的分钟时间戳。
Y矩阵例如:
始发站编号 | 发车时刻 | 行程时间 | 发车时刻_min | 到达时刻_min |
---|---|---|---|---|
…… | …… | …… | …… | …… |
-1 | 10:15 | 21.0 | 615 | 636 |
1 | 10:20 | 22.0 | 620 | 642 |
…… | …… | …… | …… | …… |
数学模型
m i n z = ∑ i ∑ j λ i j + α ( 2 ∑ j λ 1 j + 1.5 ∑ j λ 2 j + ∑ j λ 3 j ) , i = 0 , 1 , 2 min z = \sum_{i}\sum_{j} \lambda_{ij} + \alpha(2\sum_{j} \lambda_{1j} +1.5\sum_{j} \lambda_{2j}+\sum_{j} \lambda_{3j}), i=0,1,2 minz=∑i∑jλij+α(2∑jλ1j+1.5∑jλ2j+∑jλ3j),i=0,1,2
(前半项为车辆数最少,后面项为司机数据最少)
s.t.
(保证约束1)
∑
k
∑
j
x
k
i
j
=
1
,
k
=
0
,
1
,
2
\sum_{k}\sum_{j}x_{kij} = 1,k=0,1,2
∑k∑jxkij=1,k=0,1,2
(保证约束2)
−
1
≤
∑
i
=
m
n
x
k
i
j
y
i
0
≤
1
-1 \leq \sum_{i=m}^{n} x_{kij}y_{i0} \leq 1
−1≤∑i=mnxkijyi0≤1,
1
≤
m
<
n
≤
N
,
k
=
0
,
1
,
2
1 \leq m < n \leq N , k=0,1,2
1≤m<n≤N,k=0,1,2
(保证约束3)
x
k
i
j
(
y
i
2
+
y
i
3
)
<
x
k
m
j
y
m
4
+
(
1
−
x
k
m
j
)
×
1440
,
k
=
0
,
1
,
2
x_{kij}(y_{i2} + y_{i3}) < x_{kmj}y_{m4} + (1 - x_{kmj}) \times 1440, k=0,1,2
xkij(yi2+yi3)<xkmjym4+(1−xkmj)×1440,k=0,1,2
(保证约束4,并做前后两小时的松弛)
∑
i
x
1
i
j
y
i
2
{
=
0
,
≥
14
a
n
d
≤
18
\sum_{i}x_{1ij}y_{i2} \begin{cases} = 0, & \text{} \\ \geq 14 and \leq 18 \end{cases}
i∑x1ijyi2{=0,≥14and≤18
∑
i
x
2
i
j
y
i
2
{
=
0
,
≥
10
a
n
d
≤
14
\sum_{i}x_{2ij}y_{i2} \begin{cases} = 0, & \text{} \\ \geq 10 and \leq 14 \end{cases}
i∑x2ijyi2{=0,≥10and≤14
∑
i
x
3
i
j
y
i
2
{
=
0
,
≥
6
a
n
d
≤
10
\sum_{i}x_{3ij}y_{i2} \begin{cases} = 0, & \text{} \\ \geq 6 and \leq 10 \end{cases}
i∑x3ijyi2{=0,≥6and≤10
可转化为:
∑
i
x
k
i
j
y
i
2
=
α
k
(
t
k
×
60
+
β
k
)
,
k
=
0
,
1
,
2
\sum_{i}x_{kij}y_{i2} = \alpha_{k}(t_{k} \times 60 + \beta_{k}),k=0,1,2
∑ixkijyi2=αk(tk×60+βk),k=0,1,2
α
k
(
α
k
−
1
)
=
0
\alpha_{k}(\alpha_{k}-1) = 0
αk(αk−1)=0
β
k
>
0
\beta_{k} > 0
βk>0
β
k
<
180
\beta_{k} < 180
βk<180
t
1
=
14
,
t
2
=
10
,
t
3
=
6
t_{1} = 14, t_{2} = 10, t_{3} = 6
t1=14,t2=10,t3=6
进一步,由于线性规划不允许出现变量相乘的项,上述模型转化为:
∑
i
x
k
i
j
y
i
2
{
=
0
,
if
β
k
=
0
≥
t
0
k
a
n
d
≤
t
1
k
,
if
β
k
=
1
\sum_{i}x_{kij}y_{i2} \begin{cases} = 0, & \text{if $\beta_k=0$} \\ \geq \text{$t_{0k}$} and \leq \text{$t_{1k}$}, & \text{if $\beta_k=1$} \end{cases}
i∑xkijyi2{=0,≥t0kand≤t1k,if βk=0if βk=1
t
i
k
t_{ik}
tik为
类型 | 上下午班 | 全天班 | 高峰班 |
---|---|---|---|
最短工作时间 | 12 | 9 | 6 |
最长工作时间 | 17 | 13 | 9 |
(因为计算中仅考虑了司机的运营时长,没有把司机的休息时间考虑进去,所以工作时间的上下阈值均降低,并根据实际排班表进行浮动调整)
(保证约束5)
−
1
≤
∑
i
=
0
n
x
k
i
j
y
i
0
≤
0
-1 \leq \sum_{i=0}^{n} x_{kij}y_{i0} \leq 0
−1≤∑i=0nxkijyi0≤0,
1
≤
n
≤
N
,
k
=
0
,
1
,
2
1 \leq n \leq N , k=0,1,2
1≤n≤N,k=0,1,2
(浙大站的编号是-1)
(转换目标函数为线性模型)
M
=
24
×
60
=
1440
M = 24\times60 = 1440
M=24×60=1440
m
=
1
m = 1
m=1
∑
i
x
k
i
j
≤
λ
k
j
M
\sum_{i}x_{kij} \leq \lambda_{kj} M
∑ixkij≤λkjM
∑
i
x
k
i
j
≥
λ
k
j
m
\sum_{i}x_{kij} \geq \lambda_{kj} m
∑ixkij≥λkjm
(表示如果第k类车编号为 j 的车安排运营,则
λ
k
j
\lambda_{kj}
λkj为1,否则为0)
python调用ortools代码
from __future__ import print_function
from ortools.sat.python import cp_model
import pandas as pd
import matplotlib.pyplot as plt
import datetime
import numpy as np
def create_data_model(timetable):
# 规定参数数量和常量
data = {}
data['timetable'] = timetable.values.tolist()
data['num_var'] = len(timetable)
return data
# 建立整数规划模型
from __future__ import print_function
from ortools.sat.python import cp_model
import pandas as pd
import matplotlib.pyplot as plt
import datetime
import numpy as np
# 处理公交发车时刻表
def create_data_model():
'''
处理发车时刻表
:return: data
data['timetable']: list, 处理后的发车时刻表, 列内容分别为[始发站编号,发车时间,行程时间,发车时间_记为分钟,到达时间_记为分钟]
data['num_var']: int, 发车时刻的数量
'''
# 规定参数数量和常量
timetable = pd.read_excel("发车时刻表.xlsx")
# 处理得到发车时刻表
timetable_y = timetable[["start_y", "duration_y"]].dropna()
timetable_z = timetable[["start_z", "duration_z"]].dropna()
timetable_y.rename(columns={"start_y": "start", 'duration_y': "duration"}, inplace=True)
timetable_z.rename(columns={"start_z": "start", 'duration_z': "duration"}, inplace=True)
timetable_y['type'] = 1
timetable_z['type'] = -1
timetable = pd.concat([timetable_y, timetable_z], axis=0)
timetable["arrive_stamp"] = timetable[["start", "duration"]].apply(lambda x: x[0].hour * 60 + x[0].minute + round(x[1]),
axis=1)
timetable["start_stamp"] = timetable["start"].apply(lambda x: x.hour * 60 + x.minute)
timetable.sort_values(by=['start'], ascending=True, inplace=True)
timetable.reset_index(drop=True, inplace=True)
timetable = timetable[['type', 'start', "duration", 'start_stamp', 'arrive_stamp']]
data = {}
data['timetable'] = timetable.values.tolist()
data['num_var'] = len(timetable)
return data
# 建立整数规划模型
def main():
data = create_data_model()
# 建立整数规划对象
model = cp_model.CpModel()
# 建立变量x
## 班车排班变量
kk1 = 4 # 上下午班车辆数目
kk2 = 6 # 全天班车辆数量
kk3 = 3 # 高峰班车辆数量
## 车辆发车时刻表01矩阵,行为发车时刻编号,列为车辆编号,元素xij表示第i时刻第j辆车是否发车(0表示不发车,1表示发车)
### 上下午班发车时刻表矩阵
work_all = [[0 for i in range(kk1)] for j in range(data['num_var'])]
for i in range(data['num_var']):
for j in range(kk1):
work_all[i][j] = model.NewBoolVar("work_all_%i_%i"% (i, j))
### 全天班发车时刻表矩阵
work_quan = [[0 for i in range(kk2)] for j in range(data['num_var'])]
for i in range(data['num_var']):
for j in range(kk2):
work_quan[i][j] = model.NewBoolVar("work_quan_%i_%i"% (i, j))
### 高峰班发车时刻表矩阵
work_gaofeng = [[0 for i in range(kk3)] for j in range(data['num_var'])]
for i in range(data['num_var']):
for j in range(kk3):
work_gaofeng[i][j] = model.NewBoolVar("work_gaofeng_%i_%i"% (i, j))
## 辅助变量
bool_param_all = []
bool_param_all_2 = []
for i in range(kk1):
bool_param_all.append(model.NewBoolVar("bool_param_all_%i"% (i)))
bool_param_all_2.append(model.NewBoolVar("bool_param_all_2_%i" % (i)))
bool_param_quan = []
bool_param_quan_2 = []
for i in range(kk2):
bool_param_quan.append(model.NewBoolVar("bool_param_quan_%i" % (i)))
bool_param_quan_2.append(model.NewBoolVar("bool_param_quan_2_%i" % (i)))
bool_param_gaofeng = []
bool_param_gaofeng_2 = []
for i in range(kk3):
bool_param_gaofeng.append(model.NewBoolVar("bool_param_gaofeng_%i" % (i)))
bool_param_gaofeng_2.append(model.NewBoolVar("bool_param_gaofeng_2_%i" % (i)))
# 输入约束
## 禁止同站连续发车约束
for k in range(kk1):
for i in range(0, data['num_var']-1):
for j in range(i+1, data['num_var']):
model.Add(2 > sum(work_all[s][k] * data['timetable'][s][0] for s in range(i, j + 1)))
model.Add(sum(work_all[s][k] * data['timetable'][s][0] for s in range(i, j+1)) > -2)
for k in range(kk2):
for i in range(0, data['num_var']-1):
for j in range(i+1, data['num_var']):
model.Add(sum(work_quan[s][k] * data['timetable'][s][0] for s in range(i, j+1)) < 2 )
model.Add(sum(work_quan[s][k] * data['timetable'][s][0] for s in range(i, j + 1)) > -2)
for k in range(kk3):
for i in range(0, data['num_var']-1):
for j in range(i+1, data['num_var']):
model.Add(sum(work_gaofeng[s][k] * data['timetable'][s][0] for s in range(i, j+1)) < 2 )
model.Add(sum(work_gaofeng[s][k] * data['timetable'][s][0] for s in range(i, j + 1)) > -2)
## 发车时间连续约束
for k in range(kk1):
for i in range(0, data['num_var'] - 1):
for j in range(i + 1, data['num_var']):
model.Add(work_all[i][k] * data['timetable'][i][4] < work_all[j][k] * data['timetable'][j][3] + (
1 - work_all[j][k]) * 24 * 60)
# for j in range(i + 1, data['num_var'] - 1):
# model.Add(work_all[i][k] * data['timetable'][i][3] >= work_all[j+1][k] * data['timetable'][j][4])
for k in range(kk2):
for i in range(0, data['num_var'] - 1):
for j in range(i + 1, data['num_var']):
model.Add(work_quan[i][k] * data['timetable'][i][4] < work_quan[j][k] * data['timetable'][j][3] + (
1 - work_quan[j][k]) * 24 * 60)
# for j in range(i + 1, data['num_var'] - 1):
# model.Add(work_quan[i][k] * data['timetable'][i][3] >= work_quan[j+1][k] * data['timetable'][j][4])
for k in range(kk3):
for i in range(0, data['num_var'] - 1):
for j in range(i + 1, data['num_var']):
model.Add(work_gaofeng[i][k] * data['timetable'][i][4] < work_gaofeng[j][k] * data['timetable'][j][3] + (
1 - work_gaofeng[j][k]) * 24 * 60)
## 每次的始发站是浙大站
for k in range(kk1):
for j in range(1, data['num_var']):
model.Add(sum(work_all[s][k] * data['timetable'][s][2] for s in range(0, j)) < 1)
for k in range(kk2):
for j in range(1, data['num_var']):
model.Add(sum(work_quan[s][k] * data['timetable'][s][2] for s in range(0, j)) < 1)
for k in range(kk3):
for j in range(1, 69):
model.Add(sum(work_gaofeng[s][k] * data['timetable'][s][2] for s in range(0, j)) < 1)
for j in range(136, data['num_var']):
model.Add(sum(work_gaofeng[s][k] * data['timetable'][s][2] for s in range(135, j)) < 1)
## 高峰车仅在特殊时段使用
for k in range(kk3):
model.Add(sum(work_gaofeng[j][k] for j in range(69, 136)) == 0)
## 每个班次都有车辆
for i in range(0, data['num_var']):
model.Add((sum(work_all[i]) + sum(work_quan[i]) + sum(work_gaofeng[i])) == 1)
## 上下午班工作时间为16小时,全天班为12小时,早晚高峰班为8小时
for k in range(kk1):
model.Add(sum(work_all[s][k] * round(data['timetable'][s][1]) for s in range(0, data['num_var'])) < 17*60)
# model.Add(
# sum(work_all[s][k] * round(data['timetable'][s][1]) for s in range(0, data['num_var'])) == (
# 14 * 60 + param_all[k]))
# model.AddBoolXOr([param_all[k] == -14*60, param_all[k] > 0])
model.Add(sum(work_all[s][k] * round(data['timetable'][s][1]) for s in
range(0, data['num_var'])) > 12 * 60).OnlyEnforceIf(bool_param_all[k])
model.Add(sum(work_all[s][k] * round(data['timetable'][s][1]) for s in
range(0, data['num_var'])) == 0).OnlyEnforceIf(bool_param_all[k].Not())
for k in range(kk2):
model.Add(sum(work_quan[s][k] * round(data['timetable'][s][1]) for s in range(0, data['num_var'])) < 13*60)
model.Add(sum(work_quan[s][k] * round(data['timetable'][s][1]) for s in
range(0, data['num_var'])) > 9 * 60).OnlyEnforceIf(bool_param_quan[k])
model.Add(sum(work_quan[s][k] * round(data['timetable'][s][1]) for s in
range(0, data['num_var'])) == 0).OnlyEnforceIf(bool_param_quan[k].Not())
for k in range(kk3):
model.Add(sum(work_gaofeng[s][k] * round(data['timetable'][s][1]) for s in range(0, data['num_var'])) < 9*60)
model.Add(sum(work_gaofeng[s][k] * round(data['timetable'][s][1]) for s in
range(0, data['num_var'])) > 6 * 60).OnlyEnforceIf(bool_param_gaofeng[k])
model.Add(sum(work_gaofeng[s][k] * round(data['timetable'][s][1]) for s in
range(0, data['num_var'])) == 0).OnlyEnforceIf(bool_param_gaofeng[k].Not())
## 建立目标函数的辅助约束
M = 24*60
m = 1
for k in range(kk1):
model.Add(sum(work_all[:][k]) <= bool_param_all_2[k] * M)
model.Add(sum(work_all[:][k]) >= bool_param_all_2[k] * m)
for k in range(kk2):
model.Add(sum(work_quan[:][k]) <= bool_param_quan_2[k] * M)
model.Add(sum(work_quan[:][k]) >= bool_param_quan_2[k] * m)
for k in range(kk3):
model.Add(sum(work_gaofeng[:][k]) <= bool_param_gaofeng_2[k] * M)
model.Add(sum(work_gaofeng[:][k]) >= bool_param_gaofeng_2[k] * m)
# 建立目标函数
model.Minimize(sum(bool_param_all_2) * 30 + \
sum(bool_param_quan_2) * 25 + \
sum(bool_param_gaofeng_2) * 20)
# 求解目标函数
solver = cp_model.CpSolver()
# 设置程序执行时间
# solver.parameters.max_time_in_seconds = 5000
# 设置程序调用内核数量
solver.parameters.num_search_workers = 4
solution_printer = cp_model.ObjectiveSolutionPrinter()
status = solver.SolveWithSolutionCallback(model, solution_printer)
print('求解状态 = %s' % solver.StatusName(status))
print('可行解数量: %i' % solution_printer.solution_count())
# 打印排班结果,并输出至文件
output = []
for i in range(data["num_var"]):
output_tmp = [solver.Value(work_all[i][0]), solver.Value(work_all[i][1]), solver.Value(work_all[i][2]), solver.Value(work_all[i][3])
, solver.Value(work_quan[i][0]), solver.Value(work_quan[i][1]), solver.Value(work_quan[i][2])
, solver.Value(work_quan[i][3]), solver.Value(work_quan[i][4]), solver.Value(work_quan[i][5])
, solver.Value(work_gaofeng[i][0]), solver.Value(work_gaofeng[i][1]), solver.Value(work_gaofeng[i][2])]
output.append(output_tmp)
print(output_tmp)
output_df = pd.DataFrame(output, columns=["上下午班1", "上下午班2", "上下午班3", "上下午班4"
, "全天班1", "全天班2", "全天班3", "全天班4", "全天班5", "全天班6"
, "高峰班1", "高峰班2", "高峰班3"])
output_df.to_excel("最终排班结果.xlsx")
if __name__ == "__main__":
main()
存在问题
该模型执行的时间复杂度极高,难以在预期时间内得到结果,目前还不清楚如何改进其复杂度,有待进一步学习。
其他资源
优化 | 线性规划和整数规划的若干建模技巧
google or-tools的复杂排班程序深度解读
线性规划之Google OR-Tools 简介与实战
ortools 官网