本讲定位:图论系列第六讲。第 05 讲的 Dijkstra 有一个致命前提——边权非负,那个 3 个点的反例已经把它推翻了。本讲给出替代方案:既然不能贪心地”定”点,那就别定了,把每条边老老实实松弛够次数。这就是 Bellman-Ford,以及它的队列优化 SPFA。本讲做了六组实测:SPFA 与 Bellman-Ford 在 8000 组负权图上结果一致;$n-1$ 轮这个上界是紧的——同一条链,边倒序给出时少跑一轮就得不到答案,边顺序给出时 2 轮就够(差别全在边的给出顺序);“最多经过 $k$ 条边”不备份数组实测错 6.9%,最小反例只要 3 个点 2 条边;SPFA 的两面性——随机稀疏图上它比 Dijkstra 快 3 倍,网格图上慢 49 倍(出队次数达点数的 366.8 倍);判负环三法全部正确,但速度差了三个数量级,DFS 版比”入队次数”版快 2800 倍;最后是一个容易被忽略的陷阱——负环上
dis会一直减到int溢出,然后 SPFA 反而报告”无负环”(实测 $200\times200$ 网格上跑了 19.84 秒后给出错误答案)。文中所有代码经g++ -O2 -std=c++14 -Wall编译运行,P3385 与洛谷官方样例核对通过。前置知识:第 05 讲的松弛操作与 Dijkstra(必须先学,本讲从它的失败讲起);第 05 讲 6.3 节那张”三种写法在负权下表现不同”的表——本讲第 4 节就是它的展开。第 01 讲的邻接表;
queue的基本用法。适合对象:第 2、4、5 节标 J(CSP-J 复赛偶尔出现负权/判负环题);第 3 节(限边数)标 S;本讲整体的重要性不在考频,而在于它是差分约束(番外 A)的唯一执行引擎,也是理解”为什么正权一律用 Dijkstra”的必经之路。
第 05 讲 6.1 节的反例:$1\to2$ 权 $2$,$1\to3$ 权 $1$,$2\to3$ 权 $-3$。真实 $dis[3] = -1$,而 Dijkstra 给出 $1$。
病根在 05 讲 3.2 节的正确性证明里那一句:
“$P$ 上从 $x$ 到 $u$ 的那一段长度 $\ge 0$(这里用到了边权非负)”
有负边时,“当前 dis 最小的点”不再保证已经最优——后面可能冒出一条负边,让一条看起来更远的路反而更短。贪心的地基塌了。
那就别贪心了。回到第 05 讲第 2 节那个更朴素的事实:
只要
dis数组里每个值都对应一条真实路径,它就是上界;当没有任何边还能被松弛时,dis就是答案。
于是有了一个笨到不能再笨的办法:把所有边反复松弛,直到松不动为止。 这就是 Bellman-Ford。剩下的唯一问题是:要松弛多少轮才够?
struct E { int u, v, w; };
vector<E> es; // 直接存边表,不需要邻接表
memset(dis, 0x3f, sizeof(dis));
dis[s] = 0;
for (int it = 1; it <= n - 1; it++) { // 跑 n-1 轮
bool upd = false;
for (size_t i = 0; i < es.size(); i++) {
int u = es[i].u, v = es[i].v, w = es[i].w;
if (dis[u] < INF && dis[u] + w < dis[v]) { // 松弛(注意判 dis[u] 可达)
dis[v] = dis[u] + w;
upd = true;
}
}
if (!upd) break; // 本轮一次都没松弛动,提前结束
}
复杂度 $O(nm)$。代码比 Dijkstra 还短,而且不需要邻接表,直接遍历边表就行——这是它在差分约束里好用的原因之一。
关键结论:无负环时,最短路最多经过 $n-1$ 条边。
理由一句话:最短路上不会出现重复的点。若重复了,说明路径里含一个环;这个环的权值 $\ge 0$(无负环),去掉它只会更短或一样长——所以总存在一条不含环的最短路。不含环的路径最多有 $n$ 个点,也就最多 $n-1$ 条边。
再看每一轮松弛做了什么:用归纳法——