土法炼钢 · 系统与基础设施

Bellman-Ford 与路由协议:负环、SPFA 与距离向量收敛

文章导航

分类入口
algorithmsnetwork
标签入口
#bellman-ford#shortest-path#negative-cycle#spfa#rip#distance-vector#bgp#babel#eigrp#ospf

目录

Bellman-Ford 最容易被误读成“比 Dijkstra 慢、但能处理负边”的教材算法。这个说法漏掉了两个工程上更重要的点:第一,反复松弛给了它一个明确的失败信号——从源点可达的负权环;第二,同一个递推式可以在没有全局拓扑的网络里分布式运行,于是距离向量路由会继承它的收敛优点,也会暴露 count-to-infinity 这样的坏消息传播问题。

本文先讲集中式 Bellman-Ford 的不变式、提前停止和负环提取;再把松弛式改写成距离向量路由,逐条对照 RIP、Babel、EIGRP、BGP 与 OSPF 的规范;最后给出两个复现程序:bellman_ford_check.c 对拍 Floyd-Warshall,dv_sim.py 模拟 count-to-infinity 并重画本文的两张图。

一、问题、谱系与不变式

设有向图 \(G=(V,E)\),\(n=|V|\),\(m=|E|\),边权 \(w:E\to\mathbb{R}\),源点为 \(s\)。单源最短路要计算每个顶点 \(v\) 的

\[ \delta(s,v)=\min_{p:s\leadsto v}\sum_{e\in p}w(e), \]

前提是不存在从 \(s\) 可达且还能影响 \(v\) 的负权环;否则最短距离可以沿环无限下降,不是一个有限数。

Bellman-Ford 的历史比常见命名更分叉:Ford 的 RAND Paper P-923《Network Flow Theory》(1956)已经给出网络流和最短路背景下的松弛方法;Bellman 在 Quarterly of Applied Mathematics 16:87-90 的《On a routing problem》(1958)用动态规划方程表述路由问题;Moore 的《The shortest path through a maze》(1959)给出相近的逐层/队列式视角。因此文献中也常见 Bellman-Ford-Moore 的叫法。

核心操作只有一条边的松弛:

\[ \text{if } d[v] > d[u] + w(u,v),\quad d[v]\leftarrow d[u]+w(u,v),\quad \pi[v]\leftarrow u. \]

第 \(k\) 轮扫描完所有边后,标准不变式要写成两层:若从 \(s\) 到 \(v\) 存在一条至多 \(k\) 条边的路径,\(d[v]\) 不大于这些路径长度的最小值;同时,任何有限的 \(d[v]\) 始终等于某条从 \(s\) 到 \(v\) 的真实路径长度,因此它是最短距离的上界。不能说第 \(k\) 轮后“正好等于至多 \(k\) 边路径最小值”,因为同一轮内后面的边可以继续使用前面刚更新的距离。没有负权环时,一条简单最短路最多有 \(n-1\) 条边,所以 \(n-1\) 轮后收敛。若第 \(n\) 轮仍能松弛某条边,那么某条可达路径在超过 \(n-1\) 条边后还变短,路径中必含负权环。

提前停止只改变实际轮数,不改变最坏复杂度。某一轮没有任何松弛时,下一轮也不会再有松弛,可以立即返回;但有输入会让每轮只推进一小步,最坏仍是 \(O(nm)\) 次边检查。

二、负权环不仅要检测,还要提取

负权环的工程含义取决于模型:差分约束里代表不等式矛盾,外汇图里代表忽略手续费和滑点后的套利机会,最短路服务里代表请求本身无定义。只返回一个布尔值通常不够,至少要能拿到一条环作为诊断证据。

一个从源点可达的负权环:A→B、B→C、C→A 三条红边的权重和为 -3,因此从 S 到 T 的距离可以被无限降低

提取方法是 Bellman-Ford 的一个小技巧。第 \(n\) 轮记录最后被更新的顶点 \(x\),沿前驱指针走 \(n\) 步,必然进入某个负权环;再从这个点沿前驱走到重复即可得到环。本文的 C 程序在例图上输出 2->3->1->2,对应权重 \(-1-1-1=-3\)。

需要注意两个边界:

下面是可运行实现里的核心片段(完整代码见 reproduce/bellman_ford_check.c):

for (int r = 1; r <= n - 1; r++) {
    int changed = 0;
    for (int i = 0; i < m; i++) {
        if (dist[e[i].u] != INF && dist[e[i].u] + e[i].w < dist[e[i].v]) {
            dist[e[i].v] = dist[e[i].u] + e[i].w;
            pred[e[i].v] = e[i].u;
            changed = 1;
        }
    }
    if (!changed) break;
}

int x = -1;
for (int i = 0; i < m; i++) {
    if (dist[e[i].u] != INF && dist[e[i].u] + e[i].w < dist[e[i].v]) {
        pred[e[i].v] = e[i].u;
        x = e[i].v;
    }
}
if (x >= 0) {
    for (int i = 0; i < n; i++) x = pred[x];
    /* x is now on a reachable negative cycle. */
}

整数实现要防溢出:INF 不能取 LLONG_MAX 后直接参与加法;本文程序用 long long 和足够小的哨兵,并在 dist[u] != INF 后才加边权。

三、SPFA、Yen 改进与负权最短路前沿

SPFA(Shortest Path Faster Algorithm)把“每轮扫所有边”换成“只处理距离刚变小的顶点的出边”:顶点 \(u\) 的距离被改小后入队,出队时松弛所有出边。段凡丁 1994 年发表于《西南交通大学学报》的《关于最短路径的 SPFA 快速算法》使这个名字在中文语境里流行;队列式松弛思想本身可追溯到 Moore 等更早工作。

SPFA 的优点是能在许多稀疏、随机、实际变化不大的图上少做大量无效扫描;缺点是最坏复杂度仍为 \(O(nm)\)。这个界不是只来自“大 O 保守估计”:Ahuja、Magnanti、Orlin 的《Network Flows》(1993)按 label-correcting 框架给出 \(O(nm)\) 界;Cherkassky、Goldberg、Radzik(Mathematical Programming, 1996)专门比较各种队列规则,并把 FIFO 队列放在会退化的 label-correcting 家族里。直观的对抗思路是让顶点先以较差标签出队并扫描大量出边,再被更靠前的顶点改小标签、重新入队,如此重复 \(\Theta(n)\) 层。它适合作为工程启发式,不适合作为“平均很快所以总安全”的接口承诺。SLF(Small Label First)和 LLL(Large Label Last)等队列策略能改善一些输入上的常数,不改变最坏界。

Yen 在 1970 年给出 Bellman-Ford 的扫描顺序改进:先给顶点一个线性序,把边分为正向边和反向边;每轮按正序松弛正向边、按逆序松弛反向边。这个非自适应版本把保证轮数从 \(n-1\) 降到 \(\lceil n/2\rceil\),但每轮仍接触所有边,渐进复杂度仍是 \(O(nm)\)。

负权单源最短路长期被 Bellman-Ford 的 \(O(nm)\) 界统治,但不是没有后续进展:

工作 权重假设 代表结论 工程含义
Yen 1970 一般权重,无负环 调整松弛顺序,最坏轮数约减半 常数改进,代码仍简单
Goldberg 1995 整数权重 缩放算法处理负整数权重,复杂度依赖权值范围 理论上优于朴素 BF,但实现复杂
Bernstein、Nanongkai、Wulff-Nilsen 2022 整数负权 FOCS 2022 最佳论文,近线性随机算法 理论突破,离常规库实现仍远
Fineman 2024 实数负权 STOC 2024,\(\tilde O(mn^{8/9})\) 随机算法 首次突破一般实数负权的 \(mn\) 屏障

这张表不是选型建议。生产代码里,若权重非负,Dijkstra 仍是首选;若只有少量负边且要求全源最短路,Johnson 重赋权把一次 Bellman-Ford 和多次 Dijkstra 组合起来更常见;若输入可被对抗构造,SPFA 不能替代最坏界清晰的 Bellman-Ford。

四、从松弛式到距离向量路由

集中式 Bellman-Ford 的松弛式是

\[ d[v] = \min_{(u,v)\in E}\{d[u] + w(u,v)\}. \]

距离向量路由把方向换成“我到目的地 \(d\) 的距离由邻居通告决定”:

\[ D_x(d)=\min_{y\in N(x)}\{c(x,y)+D_y(d)\}. \]

每台路由器只知道邻居、链路代价和邻居发来的距离向量,不需要全局拓扑,也没有 OSPF 那样的统一链路状态数据库。好消息传播很快:某条更短路径出现时,沿路径逐跳降低距离即可。坏消息传播慢:某条路径失效后,旧信息可能绕一圈又回到故障点,形成临时环。

RIP v2(RFC 2453)是经典距离向量协议,关键参数都能在规范里找到:

项目 RFC 2453 位置 规范事实
算法来源 Section 3.1、3.4 RIP 基于 Bellman-Ford / distance-vector
最大距离 Section 3.2 路径上限为 15 hops,metric 16 表示不可达
split horizon / poisoned reverse Section 3.4.3 不把从某接口学来的路由原样从该接口发回,或发回 metric 16
triggered updates Section 3.4.4、3.10.1 路由变化时可触发更新,但仍要抑制过度更新
定时器 Section 3.8 典型 30 s 周期更新;180 s 未刷新则路由无效;garbage collection 120 s
报文输出 Section 3.10.2 生成 Response 时按接口和策略过滤/毒化条目

“RIP 的无穷是 16”不是随手取的小常数,而是用有限上界换收敛保证:当错误距离一路增加到 16,路由就被认定不可达。代价也很明显,RIP 不适合直径超过 15 跳的网络。

五、count-to-infinity:两点能修,三角仍会坏

最小例子是 A–B–C,目的地为 A。A-B 断开后,B 先把到 A 的距离设为 16;但 C 还保留“经 B 到 A,距离 2”的旧消息。若 B 采信 C,就得到距离 3;下一轮 C 又采信 B,变成 4;如此直到 16。这不是负权环,而是分布式系统中旧状态绕路返回。

reproduce/dv_sim.py 用同步轮次模拟这个过程,metric 上限固定为 RIP 的 16。下图的纵轴是系统中最大的通告 metric:

count-to-infinity 模拟曲线:两节点环路在无 split horizon 时 14 轮才涨到 16,simple split horizon 因反向路由被省略而等到 6 个周期超时,poisoned reverse 一轮收敛;三角环路即使启用 split horizon 或 poisoned reverse 仍要 14 轮

复现结果如下:

场景 策略 到 16 所需轮数 同步消息数
B-C 两点环 无 split horizon 14 28
B-C 两点环 split horizon 6 12
B-C 两点环 poisoned reverse 1 2
A-B-C 三角环 split horizon 14 84
A-B-C 三角环 poisoned reverse 14 84

simple split horizon 不是主动毒化:它只是省略从某邻居学来的反向路由。两节点例子里,B 和 C 都收不到对方关于该目的地的条目,错误路由要等 6 个 30 秒周期、也就是 RFC 2453 Section 3.8 的 180 秒 timeout 后才失效;poisoned reverse 才会把反向路由以 metric 16 发回去,一轮打断两路由器环。三角例子是:A、B、C 形成环,故障后暂态 next-hop 为 \(A\to B\)、\(B\to C\)、\(C\to A\)。split horizon 只禁止“把路由发回给自己的下一跳”,但 A 仍能从 B 收到坏消息,B 仍能从 C 收到坏消息,C 仍能从 A 收到坏消息;poisoned reverse 也只保证两路由器环路立即破掉。RFC 2453 Section 3.4.3 明确说 poisoned reverse 会防止只涉及两个网关的环路,三节点及以上环路仍需要靠触发更新、超时和有限无穷收敛。

六、BGP、Babel、EIGRP 与 OSPF:四种防环路线

距离向量不是所有路由协议的共同祖先。几个常见协议对“没有全局视图时如何防环”给了不同答案。

BGP 是路径向量,不是 Bellman-Ford。 RFC 4271 摘要和 Section 1 说明,BGP 交换的是网络可达信息以及这条信息穿过的 AS 列表;Section 5.1.2 定义 AS_PATH,Section 9.1.2 要求 AS loop 检测扫描完整 AS path,若其中出现本地 AS 号,该路由应从 Phase 2 决策中排除。它仍会选择路径、执行策略和聚合,但不要把 BGP 简化成“跑 Bellman-Ford 的互联网版”。

Babel 是改造过的距离向量。 RFC 8966 Section 2.2 直接把 Babel 的概念算法写成 Bellman-Ford;Section 2.3 展示临时环和 count-to-infinity;Section 2.5 解释用序列号解决 starvation;Section 3.5.1 在路由维护规则中细化可行性条件(feasibility condition)和序关系,决定哪些通告可被接受。Babel 的目标不是永远选择瞬时最短路,而是在移动和无线网络里限制环路持续时间,再慢慢收敛到最优。

EIGRP/DUAL 用扩散计算保证无环。 RFC 7868 说明 EIGRP 基于距离向量技术,具体算法是 DUAL;Section 3.3 写 feasibility condition,Section 3.4 写 Update、Query、Reply 等消息。Garcia-Luna-Aceves 的 IEEE/ACM Transactions on Networking 1993 论文《Loop-Free Routing Using Diffusing Computations》给出这类算法的理论基础:当本地没有可行后继(feasible successor)时,路由器发起扩散查询,等依赖范围内的回复回来再切换,避免瞬时环。

OSPF 是链路状态。 RFC 2328 摘要说每台 OSPF 路由器维护描述 AS 拓扑的链路状态数据库,并从该数据库计算最短路径树;Section 2.2 与 Section 16.1 描述 shortest-path tree 计算。它的典型实现使用 Dijkstra,而不是分布式 Bellman-Ford。链路状态的代价是泛洪 LSAs、维护一致数据库和处理区域;收益是坏消息不必逐跳猜测,拓扑变化后可以重新计算全局最短路径树。

协议 类型 环路控制 主要代价
RIP 距离向量 split horizon、poisoned reverse、triggered updates、metric 16 坏消息慢,直径受限
Babel 距离向量改造 可行性条件、序列号、请求机制 可能先保无环再慢慢到最优
EIGRP 距离向量 + DUAL 可行后继、Query/Reply 扩散计算 协议状态机更复杂
BGP 路径向量 AS_PATH 防 AS 级环路,策略优先 路径属性和策略复杂,收敛可慢
OSPF 链路状态 LSA 泛洪后本地 Dijkstra 需要同步链路状态数据库

七、复现实验:C 对拍与同步距离向量模拟

本文不写墙钟 benchmark,因为这篇的关键不是“谁快几毫秒”,而是轮数、松弛次数、消息数这类与机器负载无关的指标。复现环境如下:

运行命令:

cd post/algorithms/44-bellman-ford-routing/reproduce
gcc -O2 -Wall -Wextra -o bellman_ford_check bellman_ford_check.c
taskset -c 10 ./bellman_ford_check > bf-results.txt
gcc -O1 -g -fsanitize=address,undefined -Wall -Wextra -o bellman_ford_check_asan bellman_ford_check.c
taskset -c 10 ./bellman_ford_check_asan > bf-asan-results.txt
rm -f bellman_ford_check bellman_ford_check_asan
taskset -c 10 python3 dv_sim.py > dv-run.txt

bf-results.txt 的输出:

clrs_sample rounds=4 attempts=40 relaxes=7 neg=0 dist= 0 2 4 7 -2
negative_cycle rounds=4 attempts=20 relaxes=17 neg=1 cycle=2->3->1->2
fuzz_ok cases=2000 max_n=8 oracle=floyd_warshall

第一行是 CLRS 常用的 5 点 10 边样例,4 轮、40 次边检查、7 次成功松弛后提前停止,距离为 \(0,2,4,7,-2\)。第二行是本文负环图,提取到 2->3->1->2。第三行用固定随机种子生成 2000 个 \(n\le 8\) 的小图;若 Floyd-Warshall 判断源点可达负环,Bellman-Ford 必须也报告负环;否则每个顶点距离必须逐项相等。

dv-results.csv 的输出与第五节表格一致:

case,policy,rounds_to_inf,messages
chain_none,none,14,28
chain_split,split,6,12
chain_poison,poison,1,2
triangle_split,split,14,84
triangle_poison,poison,14,84

这两个程序分别覆盖本文两类结论:集中式算法的正确性由小图 oracle 对拍;距离向量的收敛轮数由明确状态机模拟。图 negative-cycle.svg 与 count-to-infinity.svg 都由 dv_sim.py 生成,避免手画数值与正文不一致。

八、工程选型与常见坑

场景 推荐做法 原因
非负权单源最短路 Dijkstra,必要时配合堆或桶 更快,证明依赖非负权
有负边、无负环、单源 Bellman-Ford;小图可尝试 SPFA 最坏界清晰,能报告负环
有负边、全源稀疏图 Johnson:一次 BF 重赋权,再多次 Dijkstra 避免对每个源点都跑 BF
差分约束可满足性 加虚拟源点,跑 BF 找负环 负环正好对应矛盾约束
简单 IGP/教学网络 RIP 可演示距离向量机制 metric 16 和 30 s 周期限制明显
动态无线/mesh Babel 这类带可行性条件和序列号的协议 先限制环路,再逐步最优

常见实现错误有四类。

第一,把“有负边”误写成“有负环”。负边本身不破坏最短路定义,负环才会让距离无下界。

第二,负环提取只从第 \(n\) 轮的更新点直接打印前驱链。若不先回溯 \(n\) 步,起点可能在通往环的尾巴上,不在环内。

第三,用 SPFA 时只统计入队次数,却忘了“从源点不可达的负环不该被报告”。入队计数法只对从源点传播到的顶点有效;全图负环检测需要加虚拟源点或逐分量处理。

第四,把 split horizon 当成完整防环方案。simple split horizon 只省略反向条目,两点回声要靠 timeout 失效;poisoned reverse 才会主动发 metric 16 打断两点环。三角环路仍会让坏消息绕另一条边回来,RIP 的 metric 16 是最后保险,不是优化。

九、参考资料

规范与文档

核心论文

实验


上一篇:Dijkstra 与 A*:非负权、启发式与工程优先队列 下一篇:最小生成树:割性质、Kruskal/Prim/Borůvka 与线性时间开放问题

相关阅读:

读完这篇,下一步读什么

优先读同系列或同问题的下一篇,把单篇消费变成主题集群。

2026-06-03 · algorithms

Dijkstra 与 A*:非负权、启发式与工程优先队列

从 Dijkstra 的 label-setting 不变式出发,解释负权反例、A* 的可采纳与一致启发式、reduced cost 等价关系,并用可复现 C 程序按扩展节点、出堆、松弛与 decrease-key 次数比较实现取舍。

2026-04-22 · network

网络工程索引

汇总本站网络工程系列文章,覆盖分层模型、以太网、IP、TCP、DNS、TLS、HTTP/2/3、CDN、BGP 与故障诊断。

2025-08-22 · network

【网络工程】全局负载均衡:GSLB、DNS 调度与 Anycast

系统讲解全局负载均衡的工程实现:GeoDNS 的原理与精度问题、Anycast 的路由收敛与故障转移、BGP 社区在流量调度中的应用、多活架构的流量切换策略,建立跨地域流量调度的完整知识体系。


By .