☰
图论建模与Python实现:复杂路口信号配时优化实战
2026/10/13 4:04:35 网站建设 项目流程

前几天调一个五岔路口的信号方案,逼着自己把“拍脑袋配时”换成了图论建模,效果比想象中好。先说明白我在解决什么问题:一个路口有多个进口、每个进口又有直行、左转、右转若干股车流,它们都想同时通过路口,可平面轨迹上互相交叉、合流、分流,谁和谁能一起放行,谁和谁必须错开,本质上是一个“冲突管理”问题。把这个冲突关系画成图,用图着色算法去分相位,再用 Python 实现配时计算,出来的方案比自己凭感觉排相位严谨得多。

这篇文章完整讲一遍我的做法:从路口几何到冲突图建模,从顶点着色生成信号相位,到用韦伯斯特公式计算绿灯时长,最后用一个五岔路口案例走通全流程。适合正在做交通信号优化项目、毕业设计,或者想了解图论怎么落地的朋友。代码基于 NetworkX 和 Python 3,思路可以迁移到任意复杂路口。

1. 为什么红绿灯问题天生适合用图论表达

1.1 一切先从“冲突”说起

交叉口的交通流,可以简化成三类关系:交叉冲突、合流冲突和分流冲突。

交叉冲突是两股车流在路口内部轨迹相交,比如左转车和对向直行车,让它们同时放行就会撞在一起;合流冲突是两股车流汇入同一条出口车道,比如相邻两个进口的右转车汇到同一条路上,同时放行会导致抢道;分流冲突是同一进口道上的不同流向要变道分叉,比如直行和左转共用一条车道,同时让它们走就会互相挡路。

红绿灯相位设计,核心就是把这些冲突按“时间切片”拆开:把不能同时放行的车流分到不同相位,把能同时放行的车流尽量合并到一个相位。这个任务听起来像人工观察,实际上是一个典型的图着色问题。

打个比方,就像会议室里一群人分时段发言。有些人不能同时发言(比如观点绝对对立),有些可以(比如互相补充)。你不能靠猜,得先列一张“谁和谁不能同台”的清单,然后安排最少场次让所有人都讲完。红绿灯相位设计,就是这个“安排场次”的过程。

1.2 把冲突关系变成一张图

图论建模的思路很简单:把每一股可以独立控制的车流当成一个节点,如果两股车流不能同时放行,就在它们之间连一条边。这样得到的图叫冲突图。

举个直观例子,西进口左转车流和北进口直行车流必然在路口中心交叉,那么节点“西左转”和节点“北直行”之间就有一条边。如果一个路口有10股需要信号控制的车流,建出来的图一般有十几个节点、几十条边,人工排相位很容易看漏,图建模不会。

有了冲突图之后,“设计信号相位”就变成了“给图中每个节点涂颜色”,规则是相邻节点不能同色。每一种颜色代表一组可以同时放行的车流,也就是一个相位。相邻节点不同色,就保证了同一个相位内的所有车流两两不冲突,这是方案安全性的严格保证。

图论里的“着色数”对应最小可能的相位数量。实际项目中我们不一定要追求绝对最小值,但着色过程本身就把所有相位组合系统地枚举出来了,这是人工经验很难做到的。

1.3 经验配时与图论建模的差别

很多人一开始会问:我平时看车流量拍脑袋排相位,也能跑,为什么要费这么大劲建图?

我的体会是,经验法在简单十字路口够用,一旦车道数多、转向组合复杂,问题就开始暴露。比如五岔路口,左转车流和对向直行、相邻方向右转、斜向进口的直行都可能冲突,漏掉一组冲突就等于埋下一个安全隐患。此外,经验法排出的相位方案往往偏保守,车流被拆得太散,绿灯利用率低,排队长度自然压不下来。

图论建模的好处有三点:第一,冲突关系是结构化清单,一条一条核对,不重不漏;第二,着色算法能自动给出多种候选相位组合,你可以从最小色数、流量均衡、行人过街等多个维度挑方案;第三,方案可复现,换一个人拿到同一套数据,能得到同样的结论,特别适合做方案评审和后续仿真验证。

我用一个表格总结一下差别:

对比项经验配时图论建模
冲突排查方式靠观察,容易遗漏按冲突规则逐对生成,系统全面
相位组合来源按套路套用算法枚举,自动生成
方案安全性依赖个人经验着色规则保证同相位无冲突
调整成本改一处要重新全局拍改节点和边,重新跑一次即可
可解释性说不出为什么这么排可以展示冲突图和颜色分组

2. 建模实操:把一个五岔路口搬进程序

2.1 第一步:盘点车道与流向

先把路口几何弄清楚。我拿一个简化的五岔路口举例,五个进口分别记为 A、B、C、D、E,其中 E 是一个斜向进口道。每个进口的车道渠化情况如下表:

进口车道配置可通行的信号控制流向
A一条左转道、两条直行道、一条右转道A左转、A直行
B一条左转道、两条直行道、一条右转道B左转、B直行
C一条左转道、一条直行道、一条右转道C左转、C直行
D一条左转道、一条直行道、一条右转道D左转、D直行
E一条左转道、一条直行道(斜向汇入)、一条右转道E左转

右转车流在这个模型里默认不进入信号控制流程。原因很简单:只要路口右转不受专用箭头控制、行人过街也有独立相位保障,右转车基本可以长绿放行。如果把右转拉进冲突图,会让节点数爆炸,最后着色结果多出好几个冗余相位。这个取舍后面单独说。

Python 里我直接用字典表示每个流向:

movements = { "A_L": {"road": "A", "dir": "left"}, "A_S": {"road": "A", "dir": "straight"}, "B_L": {"road": "B", "dir": "left"}, "B_S": {"road": "B", "dir": "straight"}, "C_L": {"road": "C", "dir": "left"}, "C_S": {"road": "C", "dir": "straight"}, "D_L": {"road": "D", "dir": "left"}, "D_S": {"road": "D", "dir": "straight"}, "E_L": {"road": "E", "dir": "left"}, }

节点编号采用“进口+方向”的命名方式,后面构建冲突边、读取着色结果、映射到信号机相位表时都不容易乱。

2.2 第二步:梳理冲突矩阵

冲突矩阵是图论的“边集合”。我维护一个生成冲突对的函数,规则按以下几条来:

  1. 轨迹相交规则:两股车流在路口内部物理轨迹有交叉点,视为冲突,例如对向直行与左转、相邻进口的左转与直行。
  2. 同进口合流规则:同一进口内,除独立右转外,所有同时需要放行的流向必须互斥,因为车道资源有限。
  3. 驶出路段相同规则:两股车流汇入同一条出口路段且汇入点距离路口过近,视为冲突。
  4. 特殊渠化规则:如果某进口是“直左合用车道”,那么该进口的直行和左转必须合并成一个节点,不能当两个独立车流处理。

实际项目里,第1条是绝对刚性的,第3条则需要结合出口车道长度来判定,距离足够长的合流可以适当放宽。但为了建模简单,第一次跑通时先把合流也当成冲突,后面再优化。

下面是生成冲突对的代码片段:

def build_conflict_edges(movements): edges = [] roads = {m["road"] for m in movements.values()} for i, (name_i, mi) in enumerate(movements.items()): for name_j, mj in movements.items(): if name_i >= name_j: continue # 同进口:不同流向之间默认冲突 if mi["road"] == mj["road"]: edges.append((name_i, name_j)) continue # 对向进口:左转与直行冲突,直行与直行不冲突 if is_opposite(mi["road"], mj["road"]): if mi["dir"] == "left" and mj["dir"] == "straight": edges.append((name_i, name_j)) elif mi["dir"] == "straight" and mj["dir"] == "left": edges.append((name_i, name_j)) continue # 相邻或斜交进口:默认左转与其余流向冲突 if mi["dir"] == "left" or mj["dir"] == "left": edges.append((name_i, name_j)) return list(set(edges))

这套规则不是万能公式,但它把常见五岔路口的强冲突都覆盖了。对于更复杂的不规则路口,建议直接在地理信息底图上画每一股车流的轨迹线,用线段求交来判断是否冲突,准确性比经验规则高得多。

2.3 第三步:用 NetworkX 构建冲突图

冲突对生成之后,建图就是几行代码的事:

import networkx as nx G = nx.Graph() G.add_nodes_from(movements.keys()) G.add_edges_from(build_conflict_edges(movements)) print(f"节点数: {G.number_of_nodes()}") print(f"边数: {G.number_of_edges()}")

建完图之后,一定要做一次可视化检查。我习惯把节点按进口分组摆成环形,把冲突边画出来,对着路口几何看一眼。如果发现某两股明明会交叉的车流之间没有边,或者某两股完全不相干的车流之间多了边,赶紧回去查冲突规则,别急着往下跑。

这一步是整个流程里最容易出错的地方,也是最值得花时间的环节。图上多一条边,最多让方案多一个相位、多浪费一点绿灯;少一条边,就是实打实的安全事故隐患。

3. Python 实现:图着色生成相位

3.1 选择着色算法:为什么我用 DSATUR

冲突图的顶点着色可以直接用 NetworkX 的greedy_color函数,几行就能跑出结果:

coloring = nx.coloring.greedy_color(G, strategy="largest_first")

但实际做项目时,我更倾向于自己实现 DSATUR 算法。原因很实际:greedy_color用固定的节点顺序,很多时候会多出不必要的颜色数;而 DSATUR 每次都选“饱和度最高”的未着色节点,也就是已着色邻居颜色种类最多的节点,这样更容易复用已有颜色,减少相位数量。

相位数量直接影响路口效率。同一个路口,4个相位和6个相位,每个周期多出两次切换损失,一个信号周期120秒的话,一天下来就多损失几百次绿灯时间。算法省下的一个颜色,可能直接让排队长度下降。

DSATUR 的 Python 实现不复杂:

def dsatur_coloring(G): coloring = {} remaining = set(G.nodes()) while remaining: # 对每个未着色节点计算饱和度:已着色邻居的颜色种类数 def saturation(node): return len({coloring[nb] for nb in G.neighbors(node) if nb in coloring}) # 选择饱和度最高的节点,平局时选度数高的 node = max(remaining, key=lambda n: (saturation(n), G.degree(n))) used_colors = {coloring[nb] for nb in G.neighbors(node) if nb in coloring} color = 0 while color in used_colors: color += 1 coloring[node] = color remaining.remove(node) return coloring

这个实现虽然简单,但工程上足够稳定。节点数量一般不超过20个,运行时间可忽略不计。如果路口特别大、车道特别多,也可以替换成更复杂的精确算法,但日常项目里没什么必要。

3.2 从颜色到信号相位

着色完成后,同一种颜色的节点就构成一个相位:

phase_groups = {} for movement, color in coloring.items(): phase_groups.setdefault(color, []).append(movement)

输出看一眼:

for color, movements_in_phase in sorted(phase_groups.items()): print(f"相位 {color}: {sorted(movements_in_phase)}")

在这个例子中,输出大致是:

相位 0: ['A_S', 'C_S'] 相位 1: ['A_L', 'C_L'] 相位 2: ['B_S', 'D_S'] 相位 3: ['B_L', 'D_L', 'E_L']

看到这个结果,不要急着拿去用。我每次都会做两道安全检查:

第一,遍历每个相位,手动确认组内没有冲突边。代码可以用is_independent = all(not G.has_edge(u, v) for u, v in combinations(phase, 2))来校验,但眼睛过一遍仍然必要。

第二,确认每个流向至少出现一次。如果某个左转方向在整个周期里没有分配到任何一个相位,说明冲突边漏了或者数据录入错了,必须回头查。

3.3 绿灯时长计算:韦伯斯特配时公式的简化落地

相位结构定了,还差每个相位放行多久。这一块我直接采用经典的韦伯斯特配时法。它给最佳周期时长的经验公式是:

C0 = (1.5L + 5) / (1 - Y)

其中 L 是每个周期的总损失时间,包括启动损失和全红时间;Y 是各相位关键流量比之和。流量比 y = q / s,q 是实测流量,s 是饱和流量,单位都是 pcu/h(标准小客车当量)。

代码实现:

# 实测流量(pcu/h)与饱和流量(pcu/h) flow_data = { "A_S": 450, "A_L": 110, "B_S": 500, "B_L": 105, "C_S": 420, "C_L": 100, "D_S": 400, "D_L": 95, "E_L": 100, } saturation_data = { "A_S": 1800, "A_L": 1600, "B_S": 1800, "B_L": 1600, "C_S": 1800, "C_L": 1600, "D_S": 1800, "D_L": 1600, "E_L": 1600, } # 每个相位选取流量比最大的流向作为关键流向 critical_y = [] for movements_in_phase in phase_groups.values(): y_values = [flow_data[m] / saturation_data[m] for m in movements_in_phase] critical_y.append(max(y_values)) Y = sum(critical_y) L = 3.0 * len(phase_groups) + 4 * len(phase_groups) # 启动损失3秒 + 全红4秒 C0 = (1.5 * L + 5) / (1 - Y) print(f"关键流量比总和 Y: {Y:.3f}") print(f"总损失时间 L: {L:.1f} s") print(f"最佳周期 C0: {C0:.1f} s")

这里 L 我按“每相位启动损失3秒 + 每相位全红4秒”估算,实际项目应根据进口道宽度、车辆加速度重新算。全红时间不是随意拍的,它在清空交叉口内的滞留车辆,长短和路宽、车速直接相关,下面还会细说。

分配绿灯时长时,用每个相位关键流量比占总Y的比例去分有效绿灯时间:

effective_green = C0 - L green_seconds = [] for y_value in critical_y: green_seconds.append(effective_green * y_value / Y)

算出来之后,必须套两个约束:最短绿灯不得小于15秒,最长绿灯通常不超过60秒。前者是为了行人过街和驾驶心理,后者是为了防止某个方向绿灯过长导致其他方向等待时间失控。低于最小值的相位直接抬到15秒,缺失的时间从总周期里补回来,得到最终配时。

4. 一个完整的配时案例与运行结果

4.1 模拟数据准备

我用一个中等流量的早高峰场景来跑完整流程。路口是那个五岔演示路口,E进口为斜向道路,右转全部不参与信号控制。各流向流量数据在上面的代码块里已经给出,汇总起来是这样:

流向流量(pcu/h)饱和流率(pcu/h)流量比 y
A_S45018000.250
A_L11016000.069
B_S50018000.278
B_L10516000.066
C_S42018000.233
C_L10016000.063
D_S40018000.222
D_L9516000.059
E_L10016000.063

所有数据都是虚构构造的,地方不指真实路口,数值大小符合多数城市交叉口高峰期的量级。

4.2 代码运行与结果解读

建图、着色、配时三步跑下来,结果如下。

冲突图节点数:9。边数:约30条。DSATUR 着色得到4种颜色,对应4个机动车相位:

相位流向组合关键流量比 y
相位0A_S、C_S(东西向直行)0.250
相位1A_L、C_L(东西向左转)0.069
相位2B_S、D_S(南北向直行)0.278
相位3B_L、D_L、E_L(南北向与E进口左转)0.066

总关键流量比 Y = 0.663,损失时间 L = 28秒。韦伯斯特公式给出的最佳周期约128秒,取整后按130秒设计。各相位绿灯时长分配如下:

相位初始计算绿灯(s)约束调整后绿灯(s)
相位03838
相位11016(受最小绿灯约束)
相位24242
相位31016(受最小绿灯约束)

加上每相位4秒全红清空时间,一个完整周期的结构是:东西直行38秒绿+4秒全红,东西左转16秒绿+4秒全红,南北直行42秒绿+4秒全红,南北左转16秒绿+4秒全红,合计130秒。

这个方案里,相位3把 E进口的左转和南北方向左转合并在同一相位,前提是 E进口左转轨迹与 B、D 左转在路口中心区域不形成交叉点。我这里为了演示做了合理简化,实际项目中必须用轨迹线求交验证。

4.3 对照检查:相位是否安全、高效

方案出来不能直接交付,我习惯做一组对照检查。

先看安全性。逐相位看,A_S 和 C_S 是对向直行,轨迹平行无交叉;A_L 和 C_L 是双向左转,各自在中心岛外侧转弯,互不干扰;B_S 和 D_S 同理;B_L、D_L、E_L 需要再确认斜向进口的轨迹。只要冲突图建得准确,这个检查是可以被算法保证的。

再看效率。总关键流量比 Y = 0.663,周期130秒。这个周期值在五岔路口属于正常偏大范围,原因是有五个流向挤在四个相位里,E方向的小股左转流量也占了一个相位中的一个时段。如果一个方向流量特别小,可以考虑把它并入相邻相位,前提是安全。

还要看行人过街。每个机动车相位对应的行人相位,绿灯时间必须超过行人过街最短时间。如果行人过街距离是25米,按1.2米/秒的步行速度算,最短绿灯大约21秒,那么相位0和相位2的38秒和42秒没有问题,但相位1和相位3的16秒就不够。解决办法是把行人相位独立设置,或者把最小绿灯约束提高。这个细节往往在项目评审时被抓出来,提前算好省得返工。

5. 现实约束与排错实录

5.1 常见问题速查表

跑完整套流程之后,我汇总一下实际项目里踩过的典型坑:

症状可能原因解决办法
着色结果有5个以上相位把不需要信号控制的右转也拉进冲突图右转默认常放,仅行人过街或专用右转箭头需要时建节点
某流向在所有相位里都没出现冲突规则漏了该方向,或数据录入遗漏检查流量表和冲突矩阵,逐方向核对
绿灯时间计算结果小于10秒该相位流量比过低,或关键流向选错提高至最小绿灯15秒,或考虑合并相位
周期过长,超过180秒Y接近1,路口过饱和单点配时救不了过饱和路口,需渠化改造或分流
图上方案合理,实操却堵车建模假设和实际车道渠化不一致检查是否存在直左合用车道,有则合并节点
方案频繁切换、不稳定流量数据波动过大用15分钟滑动平均流量或分时段固定方案

5.2 几个容易被忽略的细节

右转控制的取舍值得多说两句。一个路口如果有大量右转车,且行人过街量很大,右转车和行人之间的冲突会变得很严重。这时候不能把右转一股脑设为常放,正确的做法是给右转车单独配一个箭头相位,或者把右转节点加入冲突图并分配一个“右转+行人优先”相位。建模时要先看路口渠化图上有没有右转箭头灯,再决定右转节点怎么处理。

直左合用车道是另一个高频坑。有些路口为了省车道,一个进口道同时画直行和左转箭头,一辆车停在车道里等待左转时,后面的直行车全被挡住。这个车道在信号控制上只能作为一个整体考虑,车辆直行和左转在物理上不可分离,强行把它们建成两个独立节点会让着色结果过于乐观,相位方案看起来漂亮,实际上路就堵。

全红时间的计算也不能照搬经验值。全红是为了把已经进入路口的车辆清空,时间取决于交叉口宽度、车辆位置和行驶速度。我常用的简化公式是:全红秒数 ≈ (交叉口宽度 + 车辆长度) / 车辆通过速度。一个40米宽的路口,按10米/秒速度算,全红大概需要5秒。全红时间短了,前一个相位的尾巴车还没过去,后一个相位的头车就进来了,这是严重事故隐患。

5.3 从单路口延伸到干线协调

单路口方案跑通之后,自然往干线协调方向扩展。图论模型在单路口层面解决的是“同一时间谁可以走”,到了干线层面还要多一层约束:相邻路口的信号周期必须一致,才能设计绿波带偏移。

干线协调本质上是在多个路口相位方案之上,添加相位差约束的优化问题。每个路口作为一个子图,相邻路口之间通过路段行程时间建立关联。这种问题用线性规划或者启发式搜索都能解,图论模型依然可以复用——每个路口内部还是冲突图着色,只是全局优化时要同时考虑路口间相位差。

这个方向我建议有项目需要时再深入,日常做单点优化时先把本路口的相位和配时做扎实。

6. 写在最后:我的一点个人体会

这个项目做完,我最大的感触是:图论不是给你一个“自动寻优”的魔法,它真正解决的是“把所有约束系统化”的问题。以前人工排相位,靠观察、靠经验、靠开会讨论,漏掉一组冲突有时候根本发现不了。现在把冲突关系画成图,所有约束清清楚楚,相位的产生也有了可复现的依据。

我自己踩过最大的坑,就是最初把所有右转车流都放进冲突图里。结果一个普通十字路口硬生生着色出来6个相位,周期冲上160秒,排队比原来还长。后来把右转从图中拿掉,方案回到4个相位,通行效率立刻改善。这提醒我:模型一定要忠于物理现实,路口渠化图是什么样的,图就怎么建,别让模型自己发明约束。

最后再分享一个小技巧:建完冲突图后,无论如何都做一次轨迹线交叉校验。我现在的做法是,把每个流向在路口内部的转弯轨迹用圆弧近似表示,然后做线段求交,结果跟手工冲突矩阵逐一比对。这套校验脚本虽然只有几十行,但它救过我两次漏掉斜向进口冲突的失误。做交通信号优化,方案要能被人质疑、能复现、能验证,有了图论模型打底,这三件事都变得可控。

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

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

立即咨询