使用ortools进行公交排班,建模与编程

内容介绍

因工作需求,需要在公交发车时刻表和行程时间已经确定的情况下,完成公交车型排班和司机排班的建模,并使用Google的ortools求解器进行求解。

场景描述

某市某一条公交线路,根据客流情况已经完成双向发车时刻表的编排,并统计完成单向行程时间。考虑到该线路的实际运营情况,考虑使用三种车型:

车型特点
上下午班需要两个司机
全天班需要1.5个司机
高峰班需要一个司机

其中高峰班仅在早晚高峰期间运行。考虑到实际情况,一般一个司机只跟一辆车绑定,特殊情况下(如全天班)一个司机可与两辆车绑定。
要求在当下发车时刻表的前提下,并要求每个司机的工作时间在8小时左右,完成公交车型车次排班,要求车辆数最少、司机数目最少,并保证司机到站的休息时间。

抽象目标以及约束

目标:

  1. 车辆数最少
  2. 司机数目最少

约束:

  1. 每个发车时刻仅有一辆车发车
  2. 每辆车不能在同一车站连续发车
  3. 每辆车发车后,在到达终点站前不能再次安排发车
  4. 每辆车的运营时间在一定范围内,从而保证司机的工作时间
  5. 每辆车的始发站是浙大站

建立数学模型

模型参数说明

  1. λ i j \lambda_{ij} λij表示是否安排第 i 类(上述3类)车中的第 j 辆加入排班,如果使用则取1,否则取0

  2. α \alpha α为权重系数

  3. x k i j x_{kij} xkij 车辆发车排班0-1矩阵。k表示车辆类型(1为上下午班,2为全天班,3为高峰班),i为发车班次(按照发车时刻表),j为某类型车辆的编号
    例如: x 162 x_{162} x162表示上下班车中编号为2的车在时刻6发车

  4. 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
…………………………
-110:1521.0615636
110:2022.0620642
…………………………

数学模型

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=ijλij+α(2jλ1j+1.5jλ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 kjxkij=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 1i=mnxkijyi01, 1 ≤ m < n ≤ N , k = 0 , 1 , 2 1 \leq m < n \leq N , k=0,1,2 1m<nN,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+(1xkmj)×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} ix1ijyi2{=0,14and18
∑ 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} ix2ijyi2{=0,10and14
∑ 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} ix3ijyi2{=0,6and10
可转化为:
∑ 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(αk1)=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} ixkijyi2{=0,t0kandt1k,if βk=0if βk=1
t i k t_{ik} tik

类型上下午班全天班高峰班
最短工作时间1296
最长工作时间17139

(因为计算中仅考虑了司机的运营时长,没有把司机的休息时间考虑进去,所以工作时间的上下阈值均降低,并根据实际排班表进行浮动调整)

(保证约束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 1i=0nxkijyi00, 1 ≤ n ≤ N , k = 0 , 1 , 2 1 \leq n \leq N , k=0,1,2 1nN,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 官网

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值