☰
列车运行图自动生成系统:一张图背后的组合优化与落地路径
2026/10/15 14:13:04 网站建设 项目流程

简介:面向C++开发学习者和铁路信息化相关课程设计者,这份资源实现了一个基于MFC的列车运行图自动生成系统。系统将菜单栏与运行图显示相结合,支持维护列车时刻表、登记晚点信息,并依据时刻表动态绘制固定轴线与红蓝车次斜线,晚点输入后运行图与时刻表自动联动更新,可有效模拟调度场景中的实时调整流程。资源包共28个文件,以9个cpp源码和9个头文件为主体,附带设计文档、说明文本和演示视频,压缩包整体8.27MB,便于读者边看边练。目前已有3477人学习下载。通过阅读源码与演示,能够掌握C++图形界面编程、数据存储与刷新、以及时间轴坐标换算等关键思路,适合作为毕业设计或课程项目的直接参考与二次开发基础。

1. 列车运行图自动生成系统:一张图背后的组合优化与落地路径

一条线路一天能开多少趟车、每个车站的到发时刻、区间里怎么交会,全都压在列车运行图自动生成系统要画出的那张图上。我做过的一个市郊排图项目里,最难受的不是算法不收敛,而是系统在没有人工经验兜底时会排出一张“图面平整、实际进不了天窗”的死图,返工成本比人工排图还高。说白了,列车运行图自动生成系统的设计,关键不在画线,而在把排图员的隐性经验翻译成约束与目标函数。这篇内容适合做调度系统、线路规划、运筹算法选型的开发者,我会按需求建模、单区段跑通、大网络搜索、避坑、验证衔接这五步把方案拆开讲。

2. 把排图经验翻译成约束与目标:建模前的需求拆解

排图员看一张运行图,看到的不是“斜线”,而是列车在什么时间占用哪个区间、在哪个站停多久、在哪个站被后车越行。自动生成系统要把这套判断变成机器可读的数据结构和运算规则。建模前最该做的不是写算法,而是先把线路数据、列车数据和作业规则拆清楚。

2.1 先拆数据:列车等级、区间时分和车站股道

运行图的输入不是“开始时间”和“结束时间”,而是一堆静态基础数据。常见做法是先做一张基础数据表,把线路和列车的“硬条件”全部收进来,再交给算法使用。

数据项落到系统中成为什么来源与说明
区间长度、坡度、曲线半径、限速区间纯运行时分通过线路计算或实测标定
闭塞制式(自动闭塞/半自动闭塞)最小追踪间隔、区间互斥规则决定两列车能靠多近
车站股道数站内同时停靠列车数量上限枢纽站必须单独建模
列车等级(快/慢、跨线/本线)插入优先级、是否允许被越行快车越行慢车需要额外停站
最小停站时分车站到发时刻之差的下限上下客、技术作业决定

这张表看起来简单,但很多系统建模失败就是因为在表层面漏了数据。比如一个中间站只有两条到发线,如果算法只按区间约束排图,就会排出“区间全部满足、站台全被占满”的结果。再比如列车等级没拆,快车被慢车堵在后面一路跑到终点,旅行时间完全失真。

我一般会把列车等级拆成两层:一层决定插入顺序,一层决定越行关系。插入顺序影响的是“谁先占用区间”,越行关系影响的是“慢车在哪个站多停几分钟让快车通过”。这两层不能混在一个字段里,否则后面做启发式搜索时很难表达。

2.2 时空网格与三类基础约束:把图变成算得动的变量

运行图的标准画法是横轴时间、纵轴车站,每列车是一条从左下往右上的折线。自动生成系统要做的,就是给每条折线的每个折点确定坐标:列车在哪个站、什么时刻到达、什么时刻出发。

把图变成变量之后,基础约束其实只有三类。第一类是区间运行时分约束:从 A 站出发的时刻加上区间纯运行时分,必须等于到达 B 站的时刻。第二类是最小停站时分约束:到达时刻加上最小停站时间,不能晚于出发时刻。第三类是最小间隔约束:同一个区间里,前后两列车进入区间的时刻差、离开区间的时刻差,都必须大于等于追踪间隔;单线区段还要叠加一条“区间互斥”约束,任意时刻一个区间只允许一列车占用。

这三类约束全是四则运算和比较,任何一个运行图自动生成系统都逃不开。真正的复杂度来自它们的组合:一趟车在每个站的到发时刻改变,会像多米诺一样影响后面所有车。排图员的经验,比如“这趟车在这站多停 2 分钟,后面快车就不用被压”“这个区间 5 分钟内不能进两趟车”,本质都是在这三类约束上叠加出来的判断。

2.3 目标函数怎么定:单线、复线、混跑三种场景的取舍

模型建好之后,还要回答一个问题:同样的可行图,什么算“好”?不同线路制式的答案不一样。

场景优化目标初始权重经验
单线区段总旅行时间最小 + 会车等待时间最小旅行时间权重 1.0,会车等待权重 0.5
复线通勤总旅行时间最小 + 末车到达时刻尽量早旅行时间权重 1.0,末车到达权重 0.3
快慢混跑快车旅行时间损失最小 + 慢车待避时间最小快车损失权重 1.2,慢车待避权重 0.8

这些权重只是初始标定,不是一成不变的。我的经验是:先把所有约束设成硬约束,跑出一版可行解,再看目标函数里每一项的实际量级。比如总旅行时间以百分钟计、快车损失只有几十分钟,如果权重给反了,算法会把全部代价压到少数几趟快车上,图面一样难看。

目标函数之外,还要注意“分钟取整”问题。运行图系统里到发时刻一般按分钟取整,不排秒。这意味着所有约束计算都要在整数域上做,浮点数误差会被放大到时刻表里,后期人工看图完全无法接受。

3. 先跑通单方向区段:插入式自动排图的代码与参数

建模思路说得再清楚,也要先有一条能跑通的路。在正式引入遗传算法这类搜索算法之前,先用“插入式排图”把最小功能做出来:按顺序把列车放进时间轴,遇到冲突就顺延。这个方案不追求最优,但能快速验证约束集是否正确、参数是否合理。

3.1 输入参数从哪来:区间时分、停站时分与追踪间隔

插入式排图需要的参数很少:区间纯运行时分、各站最小停站时分、最小追踪间隔、期望发车时刻。参数值不要拍脑袋,区间运行时分可以由线路长度和限速反推,停站时分参考站台客流数据,追踪间隔由闭塞制式决定。

参数示例值说明
区间运行时分5, 7, 6, 6 分钟4 个区间各自的纯运行时间
最小停站时分2, 1, 1, 1, 2 分钟5 个车站,首末站停站长
最小追踪间隔3 分钟同方向前后车进入同一区间的间隔下限
期望发车时刻540, 545, 546, 550 分钟从当日 0 点起算的分钟数

这里的追踪间隔要区分“区间进入间隔”和“车站发车间隔”。同方向两列车,后车进入区间的时间不能早于前车离开区间的时间加追踪间隔;在车站发车时也一样。后文代码会同时检查这两个点。

3.2 插入式排图的最小实现:一段能直接跑的 Python

下面这段代码是单方向复线区段的插入式排图。每列车按期望发车时刻依次插入,检查每个区间的占用时段,发现冲突就整体顺延 1 分钟重新尝试。

# 插入式自动排图最小实现(单方向、复线区段) # 线路数据:4个区间、5个车站 SEG_TIME = [5, 7, 6, 6] # 各区间的纯运行时分(分钟) STOP_MIN = [2, 1, 1, 1, 2] # 各站最小停站时分(分钟) HEADWAY = 3 # 区间最小追踪间隔(分钟) DEP_DESIRE = [540, 545, 546, 550] # 各列车期望发车时刻(当日0点起算) def build_schedule(): n_seg = len(SEG_TIME) n_station = n_seg + 1 seg_occ = {i: [] for i in range(n_seg)} # 记录每个区间已被占用的 [起点, 终点] 分钟段 result = [] for idx, want in enumerate(DEP_DESIRE): dep = want # 当前尝试的首站发车时刻 while True: t = dep arrivals = [None] * n_station # arrivals[0] 无意义,首站只有发车 departures = [None] * n_station departures[0] = t ok = True for seg_i, run in enumerate(SEG_TIME): arrive = t + run # 进入下一个站点的时刻 # 检查该区间与已有占用是否满足追踪间隔 for start, end in seg_occ[seg_i]: if not (arrive + HEADWAY <= start or t >= end + HEADWAY): ok = False break if not ok: break arrivals[seg_i + 1] = arrive # 中间站按最小停站往后推,终到站不再发车 if seg_i + 1 < n_station - 1: t = arrive + STOP_MIN[seg_i + 1] departures[seg_i + 1] = t else: departures[seg_i + 1] = None if ok: # 把本次占用的区间时段写回占用表 for seg_i, run in enumerate(SEG_TIME): start = departures[seg_i] seg_occ[seg_i].append((start, start + run)) result.append({ "id": idx + 1, "arrivals": arrivals, "departures": departures }) break dep += 1 # 冲突则整体顺延1分钟再试 return result for row in build_schedule(): print(row["id"], row["arrivals"], row["departures"])

这段代码的关键逻辑是“冲突检查 + 整体顺延”。每趟车尝试插入时,逐个区间对比已有的占用时段,检查条件arrive + HEADWAY <= start or t >= end + HEADWAY是否成立:要么这趟车到达区间终点时,后车间隔已经大于等于前车离开时刻加追踪间隔;要么这趟车还没进入区间时,前车已经离开足够久。两个条件都不满足就说明区间被占住,整车顺延 1 分钟重跑。

需要注意两个参数:HEADWAY的取值不能小于信号闭塞间隔,否则排出来的图现场执行不了;DEP_DESIRE的顺序决定了插入顺序,后面会讲这个顺序对结果影响很大。运行结果里的arrivals和departures列表就是运行图的数据形态,画成折线就是一张最朴素的运行图。

3.3 从示例到系统:双向、单线与插入顺序的调整

上面的代码假设是复线单方向,区间互斥没有建模。实际线路大多是双向或者单线,要在seg_occ的检查里加规则:单线区段一个区间任意时刻只允许一列车占用,检查条件改成不允许新旧占用时段有任何重叠,并且间隔不小于一个安全时间;双向复线要把上下行两套时间表分别维护,只在车站到发线上做共用检查。

插入顺序是这段代码里最需要根据场景调整的地方。按期望发车时刻顺序插入,快车慢车一视同仁,结果就是先插入的车占便宜。我一般的做法是先把列车按等级排序:跨线车、直达快车先插,站站停的慢车后插。这样慢车会被快车“挤”到后面的空闲时段,更接近人工排图的结果。

还有一个容易忽略的点:插入式排图给出的解只是可行解,不是最优解,甚至不一定是最优解附近。系统设计时不要把这段代码当终点,要当“初始可行解生成器”,后面接搜索算法继续优化。如果想保留后悔药,插入每个区间占用时留一点缓冲分钟,比如追踪间隔 3 分钟实际按 4 分钟排,后面接优化算法时才有调整空间。

4. 大网络怎么收敛:遗传算法做粗排、启发式做细调

单方向 5 站 4 区间用插入式排图够用,但到了 30 个站、上百趟车的真实规模,插入顺序稍微一变,结果就差很多。这时需要引入搜索算法,在“谁在哪站让谁”这种离散决策空间里找更好的组合。

4.1 为什么大网络不先上精确求解器

很多人一听到自动生成,第一反应是把问题建模成整数规划丢给商用求解器。这个思路在小规模可行,但真实网络的难点在于组合爆炸:每趟车在每个站的到发时刻都是整数变量,区间占用和越行关系又引入了大量互相牵制的二元变量。几十个站、几百趟车跑下来,求解时间按小时算,而排图流程要求的是“几分钟内给一版初稿,人工看过再调”。

常见做法是先启发式生成可行解,再用元启发式优化。遗传算法是其中比较稳的一种,原因是它的染色体设计可以直接表达“越行位置”这类业务语义,排图员也看得懂,后面接人工微调时不会变成黑匣子。

4.2 染色体设计与解码:用越行位置表达一张图

运行图自动生成里最折腾的决策,是快车在哪里越行慢车。把这个问题单独拎出来做染色体,比把每个到发时刻都编码进染色体要高效得多。染色体可以用一个列表表示:慢车被快车越行的中间站索引。解码时把这些站的停站时间加上越行附加时分,再调第 3 章的插入式排图函数,把整张图算出来。

# 染色体解码:把越行位置翻译成停站时间 # 例:[2] 表示慢车在 2 号站(0 起算)避让快车 def decode_individual(gene, stop_min, overtake_extra=5): stop = list(stop_min) for station in gene: # 首站和终到站不做越行 if 0 < station < len(stop) - 1: stop[station] += overtake_extra return stop

overtake_extra是越行作业的附加停站时间,一般取 4~6 分钟,包含慢车等快车通过的时间加上进站、出站额外损耗。取值大小直接影响快车越行的收益:给得太小,算法倾向于到处越行;给得太大,慢车待避成本过高,算法会尽量少安排越行。我一般会先按 5 分钟试跑,再看快车旅行时间是否达标。

4.3 适应度函数与惩罚:先可行,再最优

有了染色体,还需要一个评价函数告诉算法“哪个解更好”。适应度函数要同时体现两件事:总旅行时间要小,不可行程度要惩罚。下面这个示例复用第 3 章的插入排图逻辑,把每个染色体的越行方案翻译成到发时刻表,再统计旅行时间和延时。

# 适应度评估:总旅行时间 + 不可行惩罚 def fitness(gene, base_params): stop = decode_individual(gene, base_params["stop_min"]) schedule = build_schedule_with_stop(stop, base_params) # 第3章插入函数参数化后 if schedule is None: return 99999 # 插不进去,直接给超大惩罚 total_travel = 0 max_delay = 0 for row in schedule: travel = row["arrivals"][-1] - row["departures"][0] total_travel += travel delay = travel - base_params["std_travel"] # 与图定标准旅行时分比较 max_delay = max(max_delay, delay) # 最大延时不能超过阈值,超出部分按惩罚系数累加 over = max(0, max_delay - base_params["max_allowed_delay"]) return total_travel + base_params["penalty"] * over

这里penalty的量纲必须和总旅行时间一致。如果惩罚系数给得远大于正常旅行时间的量级,算法会把所有代价压缩到少数几趟车上,宁可让一趟车晚点 60 分钟也要让其他车全部正点,结果图面看起来整体指标不错,实际运营时那趟车根本没法跑。建议先把penalty设成 0 跑一版纯旅行时间最小,再逐步加大,同时观察最大延时和总旅行时间两条曲线的变化。

4.4 运行参数怎么定:种群、代数、交叉变异

遗传算法的参数不是玄学,但确实需要按数据规模标定。下面的表格是我在几十列车的场景里比较稳妥的初始配置。

参数初始值调整经验
种群大小50~100中间站数量多时往大取
迭代代数200~500观察适应度曲线是否趋于水平
交叉率0.8单点交叉即可
变异率1 / 基因个数太高会把越行方案全部打乱
锦标赛大小3太大会过早收敛

参数调整有个比较实用的办法:固定数据规模,做一组小规模全因素实验,记录每代最优适应度和平均适应度。如果平均适应度和最优适应度差距很快缩小到接近 0,说明早熟了,要加大变异率或种群;如果 200 代还在明显下降,说明结束代数不够,可以设置早停条件。

遗传算法跑完得到的还不是最终图。生产系统里我会在 GA 输出的最优解上,再做一轮局部搜索:逐趟车尝试把到发时刻前后挪 1~3 分钟,只要不破坏约束、能进一步降低总旅行时间就保留。这一步收敛很快,经常能再压掉百分之几的总旅行时间,成本却比加大遗传代数低得多。

5. 运行图自动生成的避坑指南:五个常见翻车现场

这个方向做久了,问题大多不在算法本身,而在业务规则漏了项。下面这五种情况我都见过,写出来给你当排查清单。

5.1 天窗撞车:排出一张进不了维修时段的死图

现象:自动生成的运行图线条很整齐,但某条线路凌晨 0:00 到 4:00 的天窗时段里,列车被排进区间,或者刚好卡在天窗开始前几分钟到达不了终点站。人工发现后整版重排,之前跑几个小时的优化全部作废。

原因:天窗没有被建模成“不可用时间段”。很多实现只在目标函数里加了一个扣分项,算法计算后发现绕过天窗的代价太高,就会选择容忍,生成一个“惩罚最小但实际不可用”的解。

解决:把天窗建成硬约束,在图生成前先把不可用时段填进占用表。单方向示例里,直接在seg_occ[segment_index]初始化时加入天窗占用段(0, 240),任何插入检查都会自动避开。注意天窗结束前后要各加至少 5 分钟缓冲,防止列车刚从区间出来、施工人员已经进场的情况。

5.2 快慢车越行漏排:慢车挡了快车一路

现象:快车和慢车在同一个始发站先后发车,快车晚几分钟出发,结果一路跟在慢车后面,到终点时快车跑出了慢车的水平。排图员看一眼就说“这图不能用,快车没越行”。

原因:插入顺序按发车时刻而非列车等级。慢车先插入,占住了区间,快车进来时找不到越行位置,只能一直追踪。

解决:插入前按等级排序,快车先于慢车插入。还要在插入后加一道检查:快车全程旅行时间如果超过“区间纯运行时分之和 + 停站时分之和 + 合理余量”,就触发越行搜索。判断条件可以写成:快车进入某区间时,与前方慢车的入口时刻差小于慢车通过该区间的时间减去快车通过该区间的时间,则需要在更早的中间站安排越行。

5.3 到发线约束缺失:区间排得开,车站接不下

现象:区间追踪间隔全部满足,运行图看上去完全可行,但某个中间站同时停靠了 4 列车,车站只有 3 条到发线。调度员反馈“这图我们接不了”。

原因:建模时只检查了区间占用,没有检查车站股道占用。站内同时停靠数量超过到发线数,列车到了站没有股道可接,连停车都做不到。

解决:把车站到发线当成另一个“资源表”,和区间一样做占用登记。每列车进站后立即登记股道占用,出站时释放;同一车站任意时刻占用数不超过股道数。这个检查要晚于区间检查,因为列车先占区间再进站,进站后占用股道直到发车。

5.4 参数成了玄学:罚函数权重不对,解全变了形

现象:遗传算法跑 200 代收敛了,总旅行时间很好看,展开一看,有一趟车比其他车多跑了 70 分钟,剩下所有车都几乎贴着标准时分运行。整体指标没问题,但这趟车被牺牲得太明显。

原因:惩罚系数权重和主体目标量纲不匹配。最大延时超过了允许阈值,惩罚项数值远大于正常旅行时间差异,算法发现牺牲一趟车比让所有车都增加几分钟更“划算”。

解决:把适应度的构成拆开看,不要只盯最优值。我在系统里会输出分项统计:总旅行时间、最大延时、惩罚项占比。如果惩罚项一直占主导,说明权重或者阈值设错了。调整原则是先让所有解可行,再逐步收紧延时阈值,观察最大延时下降曲线。

5.5 只看图面不看过路指标:自动图好看但不好用

现象:自动生成的图线条疏密均匀、没有交叉冲突,看着很专业。但旅客换乘接不上:前一趟车到站 3 分钟后,换乘方向的车已经发走了;或者长途列车旅行时间被压到了工程极限,没有半点冗余,稍有波动就大范围晚点。

原因:目标函数只优化了列车方的成本,没有加入乘客换乘和服务冗余的指标。

解决:在适应度函数里加两个服务类指标:换乘接续时间(前后两车在同站的到达-发车间隔按 5~30 分钟为优窗口加权)和最大缓冲冗余(允许在区间运行时分和停站时分上增加的总余量)。这两个指标不需要权重很大,但一定要放进评价体系,否则自动图永远只在“行驶效率”上自嗨,交付时还是会被人打回来。

6. 结果的验证口径与人机衔接:让自动图真正可交付

自动生成的运行图直接拿给调度团队之前,要过一道可交付性检查。我这里有一套比较固定的做法,核心是把生成结果转成标准的数据表,再按三个指标做体检。

输出数据不要只存“到发时刻”,还要带上区间占用信息。常用格式是一张 CSV:车次、站名、到达分钟、发车分钟、是否越行站。这个表既能画图,又能直接导给仿真平台跑验证。

指标建议阈值说明
平均旅行速度不低于图定速度的 90%低于说明越行或停站安排不合理
区间占用率高峰时段不超过 85%超过说明缓冲不足
最大缓冲冗余每列车至少 3~5 分钟现场波动时需要余量

体检通过后,系统要支持人机衔接。我给自己定的规矩是:人工盯两件事,一是锁定,二是禁区。某趟车时刻已经确定不能动,就把它在数据表里标记为“锁定列车”,算法重排时不许改它的到发时刻;某区间某时段不允许排车,就把它加进硬约束,相当于第 5 章里说过的天窗处理。

人机衔接最怕的是人工改完一版图,交给算法再优化一次,又改回去了。避免的办法是把人工修订直接作为下一轮的输入约束:人工设定了某站 10:00 到 10:10 的越行安排,算法后面任何优化都不能推翻这个设定,只能在此基础上调整其他列车。这样人工经验才能在系统里积累下来,而不是每次重排都从头再来。希望这条经验能帮到正在做同类系统的人,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询