文章目录
一、书上例子
没带课本,开学再说,先给例题
二、代码实现(实例:安顺市公交调度问题)
由于运筹学大作业做的是这个,就以这个数据为母本解决问题了。感谢本地土著赵同学提供的数据
2.1 基本思路
当系统中所有线路运行达到稳定状态时,针对其中任意一条路线,在其中运行的车辆都会按时距均匀分布在其中,由此利用走完一条路线的总时间除以该路线发车间隔即可得到该路线的刚需车数。如果结果是小数则要向上取整,避免产生无法按照预定发车间隔发车的情况。
一个车站在一个周期内可能出现到达数与离开数不均等的情况,可以将某些站点多出的车辆调度到缺少车辆的站点使用,以最大化的利用系统内的所有车辆,进而求出公交系统所需的最小车辆数。这就是最小配置问题。
不同站点之间往返时间不同,因此要根据这个时间,规划求解合理的调度方式。求解的方法是:求得每个车站在单位周期内的车的余缺数,与站点之间往返的最短时间,以此匹配运输问题的产地-销地-成本模型求解。
在稳态时,调度路线上也会按时距均匀分布车辆,因此还要求得调度路线上所额外需要的调度车数。调度数的求解思路与稳态的求解思路相同。同理,结果为小数要向上取整。
为了方便,接下来仅在稳态情况下进行计算,不考虑在系统刚开始运行时的发车时序,不考虑随机因素。
将问题进行拆分,每一步都用一个函数实现,最后再打包起来。
2.2 具体解释
2.2.1 导入库
import xlrd
import math
from pymprog import *
2.2.2 数据提取
'''将excel表格导入为二维数组,必须按照格式输入!!!'''
def readexcel1(exceldir): #输入表格位置,例:'C:\\Users\\Administrator\\Desktop\\test.xlsx'
file = xlrd.open_workbook(exceldir) #用sheet,这里sheet1,路线数据
sheet1 = file.sheet_by_index(0) #获得行数
rows1 = sheet1.nrows
data = [[] for i in range(rows1)]
for i in range(rows1):
data[i] = sheet1.row_values(i)
del data[0] #删第一行
data_dict = dict() #获得字典,定义:{('起点站','终点站','路线编号'):[起点站发车间隔,起点到终点时间]}
for i in data:
data_dict[(i[1],i[2],i[0])] = [i[3],i[4]]
# print('路线数据:',data_dict,'\n')
sheet2 = file.sheet_by_index(1) #sheet2,最短时间表
rows2 = sheet2.nrows
cols2 = sheet2.ncols
r = sheet2.row_values(0) #获取第一行第一列
c = sheet2.col_values(0)
time = [[] for i in range(rows2)] #以二维列表形式输入最短时间表
for i in range(rows2):
time[i] = sheet2.row_values(i)
time_dict = dict() #获得字典,定义:{('起点站','终点站'):最小耗时}
for i in range(1,rows2):
for j in range(1,cols2):
time_dict[r[i],c[j]] = sheet2.cell_value(i,j)
# print('最短时间:',time_dict,'\n')
return data_dict,time_dict #返回两个字典
输入的表格格式:(需严格遵守)
sheet1:路线基本信息(双向路线需拆成两条单向路线)
sheet2:站点间往返最短时间
参考了python 读取excel 并存入二维列表,并进行了修改。
输入数据的方法采用excel表格。考虑到有时起终点站之间只有往没有返,或是往返时发车间隔不同,在输入站点基本信息时,按照单向线路输入,往返的线路也需要拆为两条单向线路。
首先提取数据,按照格式填写表格,在算法中利用xlrd包处理表格。输入表格位置,遍历提取表格sheet1、sheet2的内容,然后将这些数据以字典的形式储存。需要注意的是表格必须按照固定的格式输入,这样在遍历时才不会出现错误。表格涉及到时间的单位均为分钟(min)。
遍历sheet1得到的字典及格式定义:
data_dict = {(‘起点站’,‘终点站’,‘路线编号’):[起点站发车间隔,起点到终点时间]}
其键是起、终点站与路线编号组成的元组,值是发车间隔与走完整条路线所需时间组成的列表,这个字典主要用于之后求解余缺数得到运输表,其单位均为分钟。
遍历sheet2得到的字典及格式定义:
time_dict = {(‘起点站’,‘终点站’):最小耗时}
其键是起、终点站组成的元组,值是调度时车辆在两站之间穿梭所需要的最短时间,这个字典主要用在之后计算调度方法,其单位为分钟。
2.2.3 刚需求解
'''计算必需的基本车辆数'''
def basicnum_(data_dict):
num = []
basic_dict = dict() #带路线编号的具体调运方案
for (i,j,k) in data_dict:
temp = data_dict[(i,j,k)][1]/data_dict[(i,j,k)][0]
num.append(temp)
if temp == int(temp): #将整数浮点数转换为整数,让输出格式好看一点
basic_dict[int(k)] = int(temp)
else:
basic_dict[int(k)] = temp
basic_num = 0
for i in num:
basic_num += math.ceil(i) #求和时每项向上取整
# print('路线编号:车数',basic_dict,'\n')
# print('刚需(求和时每项向上取整):',basic_num,'\n')
return basic_num, basic_dict
输入data_dict字典,用for循环遍历每一项,提取其中发车间隔与起点到终点时间的数据并作商,即可求出单条路线上的刚需数,并将结果加入一个{路线编号:该路线刚需车辆}格式的字典。最后以for循环遍历该字典,将单条路线刚需单独向上取整并全部累加即可得到总刚需。最终以:
{路线编号:该路线刚需车辆}
的字典格式输出对每条路线的求解结果,并输出总刚需。在实现的过程中为了输出美观做了处理,将整数浮点数转换为整数。
2.2.4 调度车辆处理
接下来的所有函数都是为了实现调度车辆数与调度方式求解而准备的。基本的思路是输入data_dict字典,遍历字典,提取数据,进行运算,求出每个车站在稳态运行一个最小周期后的余缺数并转化为运输表,解运输表得到调度方式,然后计算调度数。
为了方便计算,先求最小周期。将用for循环遍历data_dict字典,提取所有线路的所有发车间隔,并求其最小公倍数,作为最小周期。每运行一个最小周期,每个车站的发车情况就会回归到初始状态,然后开始下一个周期。
为了方便,先实现两个功能:提取所有车站与求取最小公倍数
def station_(data_dict): #提取所有车站
temp_station = set()
for i,j,k in data_dict:
temp_station.add(i)
temp_station.add(j)
station = list(temp_station)
# print('所有车站:',station,'\n')
return station #返回车站列表
def lcm_(num): #最小公倍数Least common multiple,输入一个列表
minimum = 1
for i in num:
minimum = int(i) * int(minimum) / math.gcd(int(i), int(minimum))
return int(minimum) #返回最小公倍数
接下来还是分开介绍这部分的其他几个函数:
余缺数求解:
def sadndata_(data_dict) : #最小单位周期、各站余缺数
temp_interval = set()
for i,j,k in data_dict:
temp_interval.add(data_dict[(i,j,k)][0])
interval = list(temp_interval) #提取发车时间间隔interval
#print(interval)
#到达与出发量列表
lcm = lcm_(interval) #最小公倍发车时间(发车周期)
station = station_(data_dict) #车站序号列表
#求余缺数Surplus and deficiency number(sadn_num),做字典对应
sadn_dict = dict()
for i in station:
sadn_dict[i] = 0 #初始化站点余缺数字典
for i,j,k in data_dict: #发车周期除间隔为车数n,一个周期发站少n,到站多n
n = lcm/data_dict[(i,j,k)][0]
sadn_dict[i] = sadn_dict[i] - n
sadn_dict[j] = sadn_dict[j] + n
# print('最小周期',lcm)
# print('余缺数:',sadn_dict)
return sadn_dict, lcm, station
新建并初始化一个字典sadn_dict,格式为{车站:一个周期内该站的余缺数},初始时余缺数均为0。
for循环遍历data_dict的每条路线,对其中任意一条路线,在一个周期内,发车站会发出(n=周期/发车间隔)辆车,到达站会到达同样数量的车。对每一条路线,求取这条路线的n。在sadn_dict中的对应车站(键)如果是该路线起点站,则对应余缺数(值)-n,代表一个周期发出去n辆车。同理。对应站是终点站则其余缺数+n。
最终可以得到每个站对应的余缺数,以字典sadn_dict = {车站:一个周期内该站的余缺数}记录数据。
转化为运输表:
def proccessdata_(sadn_dict,time_dict): #处理数据
produce_dict = dict() #(产地:产量)对应关系字典
sale_dict = dict() #(销地:销量)对应关系字典
price_dict = dict() #((产地,销地):时间成本)对应关系字典
for i in sadn_dict.keys():
if int(sadn_dict[i]) > 0 :
produce_dict[i] = sadn_dict[i]
elif int(sadn_dict[i]) < 0 :
sale_dict[i] = abs(sadn_dict[i]) #销量记得转为正数
for i,j in time_dict:
if i in produce_dict.keys() and j in sale_dict.keys() :
price_dict[(i,j)] = time_dict[(i,j)]
# print("\n(产地,销地):运价", price_dict)
# print("产地:产量", produce_dict)
# print("销地:销量", sale_dict)
return produce_dict, sale_dict, price_dict #返回成本、产地、销地字典
利用得到的余缺数字典sadn_dict,以及最开始通过表格输入的最小耗时字典time_dict,可以转化格式得到运输表。
在余缺数字典中,运行一个周期后,余缺数小于0(缺车)的站作为销地,销量为其缺车的数量;而余缺数大于0(多车)的车站做为产地,产量为多车的数量。time_dict中的最小耗时为车辆在两地之中调度的成本。
同样利用for循环遍历字典,可以得到运输表字典,分别为:
(产地:产量)对应关系字典produce_dict
(销地:销量)对应关系字典sale_dict
((产地,销地):时间成本)对应关系字典price_dict
解运输表对应的运输问题与求解周转数:
#参考“pymprog库应用(五)运输问题”csdn博客,将其改写打包为函数使用
def transport(produce_dict, sale_dict, price_dict,lcm): #利用pymprog库,求解运输问题
begin('transport')
x = var('x', price_dict.keys()) # 调运方案
minimize(sum(price_dict[i, j]*x[i, j] for (i, j) in price_dict.keys()), 'Cost') # 总运费最少
for i in produce_dict.keys(): # 产地产量约束
sum(x[i, j] for j in sale_dict.keys()) == produce_dict[i]
for j in sale_dict.keys(): # 销地销量约束
sum(x[i ,j] for i in produce_dict.keys()) == sale_dict[j]
solve()
trans_num = report(x,lcm,price_dict)
end()
return trans_num
def report(x,lcm,price_dict): #与transport函数配套使用的输出函数
temp = []
plan = str() #用作返回值
# print("\n调运方案(最优之一)")
for (i, j) in price_dict.keys():
if x[i, j].primal > 0 and price_dict[i, j] != 0:
n = math.ceil(x[i, j].primal)
t = math.ceil(price_dict[i, j])
n_all = n*math.ceil(t/lcm)
temp.append((n, t))
solution = "产地:%s -> 销地:%s 调运数:%-2d 耗时:%2d 路线总周转车数:%2d" % (i, j, n, t, n_all)
plan = plan + solution +'\n'
# print(solution)
#向上取整ceil
trans_num = 0 #累加向上取整的整数周期所需值
for i in temp:
trans_num += i[0]*math.ceil(i[1]/lcm)
# print('周转数:',trans_num,'\n')
# print(temp)
return trans_num , plan
解运输表:
在这一步中,我参考了pymprog库应用(五)运输问题,需要调用pymprog库。其基本原理是输入上述三个字典,并将其对应的运输表转化为总成本最小的目标方程,与产地产量、销地销量约束条件方程组,最终求解目标方程最优解。
输出的结果为一个周期的具体周转方案:"产地: -> 销地: 调运数: 耗时: "
求解周转数:
在稳态时,周转路线上同样按时距均匀分布车辆。从上面的输出可以提取得到调运数、耗时、周期。对其中一条周转线路,当周转车从产地到销地的时间超过一个周期时,需要在周转线路上布置更多的车,以满足周转路线的均匀分布需求。而当周转时间不足一个周期时,需要准备满足一个周期余缺的车,从而在周期结束后能够归零产销地的余缺数。
对于一条周转路线,其需要准备的周转车总数为:
n = 一个周期调运的车数*[向上取整(周转路线全程耗时/周期)]
然后将n用for循环累加,即可得到周转所需总车数。
2.2.5 整理输出格式
'''最后做一个函数把前面的过程全部打包起来'''
def min_configration_project(exceldir):
data_dict,time_dict = readexcel1(exceldir)
basic_num, basic_dict = basicnum_(data_dict)
sadn_dict, lcm, station_ = sadndata_(data_dict)
produce_dict, sale_dict, price_dict = proccessdata_(sadn_dict,time_dict)
trans_num , plan = transport(produce_dict, sale_dict, price_dict,lcm)
total_num = math.ceil(basic_num) + math.ceil(trans_num) #总需求数 = 刚需 + 周转数
# print('总需求车数:', total_num)
station = '' #处理station,让它好看一点,免得以列表输出
for i in range(len(station_)):
if int(i) == len(station_)-1:
station = station + station_[i]
else:
station = station + station_[i] +'、'
output1 = '所有车站:'+str(station)+'\n\n'+'最小周期(min):'+str(lcm)+'\n\n'
output2 = '路线编号及对应刚需:'+'\n'+str(basic_dict)+'\n\n'+'刚需(逐项向上取整):'+str(basic_num)+'\n\n'
output3 = '调运方案(最优之一)'+'\n'+str(plan)+'\n'+'周转数:'+str(trans_num)+'\n\n'+'总需求车数:'+str(total_num)
output = output1 + output2 + output3 #免得太长代码一行放不下
print(output)
return output
最后使用一个新函数,打包了上述整个过程,并整理输出格式。求和得到总需求车数
最后运行这个函数即可:
exceldir = 'C:\\Users\\Administrator\\Desktop\\运筹学大作业\\bus_test.xlsx'
min_configration_project(exceldir)
三、完整代码、输出结果、检验
3.1 完整代码
公交车最小配置.py
# -*- coding: utf-8 -*-
"""
Created on Mon Nov 30 21:20:45 2020
@author: xyw
"""
import xlrd
import math
from pymprog import *
'''将excel表格导入为二维数组,必须按照格式输入!!!'''
def readexcel1(exceldir): #输入表格位置,例:'C:\\Users\\Administrator\\Desktop\\test.xlsx'
file = xlrd.open_workbook(exceldir) #用sheet,这里sheet1,路线数据
sheet1 = file.sheet_by_index(0) #获得行数
rows1 = sheet1.nrows
data = [[] for i in range(rows1)]
for i in range(rows1):
data[i] = sheet1.row_values(i)
del data[0] #删第一行
data_dict = dict() #获得字典,定义:{('起点站','终点站','路线编号'):[起点站发车间隔,起点到终点时间]}
for i in data:
data_dict[(i[1],i[2],i[0])] = [i[3],i[4]]
# print('路线数据:',data_dict,'\n')
sheet2 = file.sheet_by_index(1) #sheet2,最短时间表
rows2 = sheet2.nrows
cols2 = sheet2.ncols
r = sheet2.row_values(0) #获取第一行第一列
c = sheet2.col_values(0)
time = [[] for i in range(rows2)] #以二维列表形式输入最短时间表
for i in range(rows2):
time[i] = sheet2.row_values(i)
time_dict = dict() #获得字典,定义:{('起点站','终点站'):最小耗时}
for i in range(1,rows2):
for j in range(1,cols2):
time_dict[r[i],c[j]] = sheet2.cell_value(i,j)
# print('最短时间:',time_dict,'\n')
return data_dict,time_dict #返回两个字典
'''计算必需的基本车辆数'''
def basicnum_(data_dict):
num = []
basic_dict = dict() #带路线编号的具体调运方案
for (i,j,k) in data_dict:
temp = data_dict[(i,j,k)][1]/data_dict[(i,j,k)][0]
num.append(temp)
if temp == int(temp): #将整数浮点数转换为整数,让输出格式好看一点
basic_dict[int(k)] = int(temp)
else:
basic_dict[int(k)] = temp
basic_num = 0
for i in num:
basic_num += math.ceil(i) #求和时每项向上取整
# print('路线编号:车数',basic_dict,'\n')
# print('刚需(求和时每项向上取整):',basic_num,'\n')
return basic_num, basic_dict
'''这部分处理调度车辆'''
def station_(data_dict): #提取所有车站
temp_station = set()
for i,j,k in data_dict:
temp_station.add(i)
temp_station.add(j)
station = list(temp_station)
# print('所有车站:',station,'\n')
return station #返回车站列表
def lcm_(num): #最小公倍数Least common multiple,输入一个列表
minimum = 1
for i in num:
minimum = int(i) * int(minimum) / math.gcd(int(i), int(minimum))
return int(minimum) #返回最小公倍数
def sadndata_(data_dict) : #最小单位周期、各站余缺数
temp_interval = set()
for i,j,k in data_dict:
temp_interval.add(data_dict[(i,j,k)][0])
interval = list(temp_interval) #提取发车时间间隔interval
#print(interval)
#到达与出发量列表
lcm = lcm_(interval) #最小公倍发车时间(发车周期)
station = station_(data_dict) #车站序号列表
#求余缺数Surplus and deficiency number(sadn_num),做字典对应
sadn_dict = dict()
for i in station:
sadn_dict[i] = 0 #初始化站点余缺数字典
for i,j,k in data_dict: #发车周期除间隔为车数n,一个周期发站少n,到站多n
n = lcm/data_dict[(i,j,k)][0]
sadn_dict[i] = sadn_dict[i] - n
sadn_dict[j] = sadn_dict[j] + n
# print('最小周期',lcm)
# print('余缺数:',sadn_dict)
return sadn_dict, lcm, station
def proccessdata_(sadn_dict,time_dict): #处理数据
produce_dict = dict() #(产地:产量)对应关系字典
sale_dict = dict() #(销地:销量)对应关系字典
price_dict = dict() #((产地,销地):时间成本)对应关系字典
for i in sadn_dict.keys():
if int(sadn_dict[i]) > 0 :
produce_dict[i] = sadn_dict[i]
elif int(sadn_dict[i]) < 0 :
sale_dict[i] = abs(sadn_dict[i]) #销量记得转为正数
for i,j in time_dict:
if i in produce_dict.keys() and j in sale_dict.keys() :
price_dict[(i,j)] = time_dict[(i,j)]
# print("\n(产地,销地):运价", price_dict)
# print("产地:产量", produce_dict)
# print("销地:销量", sale_dict)
return produce_dict, sale_dict, price_dict #返回成本、产地、销地字典
#参考“pymprog库应用(五)运输问题”csdn博客,将其改写打包为函数使用
def transport(produce_dict, sale_dict, price_dict,lcm): #利用pymprog库,求解运输问题
begin('transport')
x = var('x', price_dict.keys()) # 调运方案
minimize(sum(price_dict[i, j]*x[i, j] for (i, j) in price_dict.keys()), 'Cost') # 总运费最少
for i in produce_dict.keys(): # 产地产量约束
sum(x[i, j] for j in sale_dict.keys()) == produce_dict[i]
for j in sale_dict.keys(): # 销地销量约束
sum(x[i ,j] for i in produce_dict.keys()) == sale_dict[j]
solve()
trans_num = report(x,lcm,price_dict)
end()
return trans_num
def report(x,lcm,price_dict): #与transport函数配套使用的输出函数
temp = []
plan = str() #用作返回值
# print("\n调运方案(最优之一)")
for (i, j) in price_dict.keys():
if x[i, j].primal > 0 and price_dict[i, j] != 0:
n = math.ceil(x[i, j].primal)
t = math.ceil(price_dict[i, j])
n_all = n*math.ceil(t/lcm)
temp.append((n, t))
solution = "产地:%s -> 销地:%s 调运数:%-2d 耗时:%2d 路线总周转车数:%2d" % (i, j, n, t, n_all)
plan = plan + solution +'\n'
# print(solution)
#向上取整ceil
trans_num = 0 #累加向上取整的整数周期所需值
for i in temp:
trans_num += i[0]*math.ceil(i[1]/lcm)
# print('周转数:',trans_num,'\n')
# print(temp)
return trans_num , plan
'''完成对调度车辆数目处理的函数构建'''
'''最后做一个函数把前面的过程全部打包起来'''
def min_configration_project(exceldir):
data_dict,time_dict = readexcel1(exceldir)
basic_num, basic_dict = basicnum_(data_dict)
sadn_dict, lcm, station_ = sadndata_(data_dict)
produce_dict, sale_dict, price_dict = proccessdata_(sadn_dict,time_dict)
trans_num , plan = transport(produce_dict, sale_dict, price_dict,lcm)
total_num = math.ceil(basic_num) + math.ceil(trans_num) #总需求数 = 刚需 + 周转数
# print('总需求车数:', total_num)
station = '' #处理station,让它好看一点,免得以列表输出
for i in range(len(station_)):
if int(i) == len(station_)-1:
station = station + station_[i]
else:
station = station + station_[i] +'、'
output1 = '所有车站:'+str(station)+'\n\n'+'最小周期(min):'+str(lcm)+'\n\n'
output2 = '路线编号及对应刚需:'+'\n'+str(basic_dict)+'\n\n'+'刚需(逐项向上取整):'+str(basic_num)+'\n\n'
output3 = '调运方案(最优之一)'+'\n'+str(plan)+'\n'+'周转数:'+str(trans_num)+'\n\n'+'总需求车数:'+str(total_num)
output = output1 + output2 + output3 #免得太长代码一行放不下
print(output)
return output
'''运行它'''
exceldir = 'C:\\Users\\Administrator\\Desktop\\运筹学大作业\\bus_test.xlsx'
min_configration_project(exceldir)
3.2 输出结果
所有车站:黔中商贸城、机场、北站、火车站、宋旗、东站、西站、豪德
最小周期(min):30
路线编号及对应刚需:
{1: 9, 2: 12, 3: 4, 4: 12, 5: 2, 6: 5, 7: 4, 8: 4, 9: 9, 10: 9, 11: 5, 12: 2, 13: 7.5, 14: 5, 15: 6, 16: 6, 17: 9, 18: 6, 19: 6, 20: 4}
刚需(逐项向上取整):127
调运方案(最优之一)
产地:豪德 -> 销地:火车站 调运数:3 耗时:30 路线总周转车数: 3
产地:机场 -> 销地:火车站 调运数:1 耗时:30 路线总周转车数: 1
产地:宋旗 -> 销地:火车站 调运数:1 耗时:30 路线总周转车数: 1
产地:西站 -> 销地:火车站 调运数:1 耗时:30 路线总周转车数: 1
周转数:6
总需求车数:133
3.3 检验:求解课本例题
为了适应算法,将该题中1天等价为60分钟进行数据输入
输入数据:
运行:
exceldir = 'C:\\Users\\Administrator\\Desktop\\运筹学大作业\\ship_test.xlsx'
min_configration_project(exceldir)
输出结果:
所有车站:d、f、e、c、a、b
最小周期(min):60
路线编号及对应刚需:
{1: 57, 2: 10, 3: 9, 4: 15}
刚需(逐项向上取整):91
调运方案(最优之一)
产地:c -> 销地:a 调运数:1 耗时:120 路线总周转车数: 2
产地:c -> 销地:e 调运数:1 耗时:300 路线总周转车数: 5
产地:d -> 销地:b 调运数:1 耗时:780 路线总周转车数:13
产地:d -> 销地:e 调运数:1 耗时:1020 路线总周转车数:17
产地:f -> 销地:e 调运数:1 耗时:180 路线总周转车数: 3
周转数:40
总需求车数:131
可以看到,结果与例题一样。(例题刚需与调度数都是整数,没有像上面的实例一样出现小数刚需车数)