Bellman-Ford 简介

Bellman-Ford 是解决 单源最短路 的经典算法。
它和 Dijkstra 最大的区别是:

  • Dijkstra 不能处理负边
  • Bellman-Ford 可以处理负边
  • 并且 Bellman-Ford 还能判负环

这里的“负环”是指:从源点可达的某个环,环上边权和小于 0。
如果存在这种环,那么最短路就不存在,因为你可以在这个环上无限绕,使路径长度越来越小。


核心思想

dist[s]=0,其余点为正无穷。

然后进行若干轮“松弛”操作。
对于每条边 u -> v (w),尝试更新:

[ dist[v] = \min(dist[v], dist[u] + w) ]

为什么这样做是对的?

因为:

  • 第 1 轮后,可以得到“至多经过 1 条边”的最短路
  • 第 2 轮后,可以得到“至多经过 2 条边”的最短路
  • n-1 轮后,可以得到“至多经过 n-1 条边”的最短路

一个不含环的最短路径,最多只会经过 n-1 条边,所以做 n-1 轮就够了。


为什么能判负环

如果做完 n-1 轮后,还能继续松弛某条边,说明存在一条“更优路径”需要经过至少 n 条边。

而一个含 n 条边的路径,在 n 个点的图中一定经过了重复点,也就是经过了环。
还能继续变优,说明这个环是负环。

因此:

  • 做完 n-1 轮后
  • 再检查一遍所有边
  • 若还能松弛,则存在 从源点可达的负环

适用场景

Bellman-Ford 常用于:

  1. 图中有负边
  2. 需要判断负环
  3. 某些题目建图后边数不算特别大
  4. 作为 SPFA 的理论基础

时间复杂度

设点数为 n,边数为 m`:

  • 时间复杂度:O(nm)
  • 空间复杂度:O(n + m)

这比堆优化 Dijkstra 的 O(m log n) 慢很多,所以在没有负边时一般不用它。


标准 OI 模板

这是最常见的竞赛写法:边集数组。

#include <bits/stdc++.h>
using namespace std;

const int N = 5005;
const long long INF = 1e18;

struct Edge {
    int u, v;
    long long w;
} e[10005];

long long dist[N];

bool bellman_ford(int n, int m, int s) {
    for (int i = 1; i <= n; i++) dist[i] = INF;
    dist[s] = 0;

    // 做 n-1 轮松弛
    for (int i = 1; i <= n - 1; i++) {
        bool updated = false;
        for (int j = 1; j <= m; j++) {
            int u = e[j].u, v = e[j].v;
            long long w = e[j].w;
            if (dist[u] != INF && dist[v] > dist[u] + w) {
                dist[v] = dist[u] + w;
                updated = true;
            }
        }
        if (!updated) break; // 提前结束优化
    }

    // 判负环:若还能松弛,则存在从 s 可达的负环
    for (int j = 1; j <= m; j++) {
        int u = e[j].u, v = e[j].v;
        long long w = e[j].w;
        if (dist[u] != INF && dist[v] > dist[u] + w) {
            return false; // 有负环
        }
    }
    return true; // 无负环
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, m, s;
    cin >> n >> m >> s;
    for (int i = 1; i <= m; i++) {
        cin >> e[i].u >> e[i].v >> e[i].w;
    }

    if (!bellman_ford(n, m, s)) {
        cout << "Negative cycle\n";
    } else {
        for (int i = 1; i <= n; i++) {
            if (dist[i] == INF) cout << "INF ";
            else cout << dist[i] << " ";
        }
        cout << '\n';
    }

    return 0;
}

模板细节解释

1. 为什么用边集而不是邻接表

Bellman-Ford 每一轮都要把 所有边扫一遍
所以直接存边最方便:

struct Edge {
    int u, v;
    long long w;
};

每轮直接枚举 e[j] 即可。


2. 为什么要写 dist[u] != INF

if (dist[u] != INF && dist[v] > dist[u] + w)

这是为了防止:

  • u 根本不可达
  • INF + w 参与运算导致错误

这是 Bellman-Ford 里必须写的判断。


3. 提前结束优化

bool updated = false;
...
if (!updated) break;

如果某一轮没有任何更新,说明最短路已经稳定了,后面不用再做。
这个优化在很多题里很有用。


一个简单例子

图:

  • 1 -> 2 (2)
  • 1 -> 3 (5)
  • 2 -> 3 (-4)

源点 1

初始:

  • dist[1]=0
  • dist[2]=INF
  • dist[3]=INF

第 1 轮松弛:

  • 1 -> 2dist[2]=2
  • 1 -> 3dist[3]=5
  • 2 -> 3dist[3]=min(5, 2+(-4))=-2

结果:

  • dist[1]=0
  • dist[2]=2
  • dist[3]=-2

这就是 Bellman-Ford 可以处理负边的体现。


和 Dijkstra 的对比

Dijkstra

  • 不能有负边
  • 速度快
  • 常用于非负权图最短路

Bellman-Ford

  • 能处理负边
  • 能判负环
  • 复杂度高

所以竞赛里一般是:

  • 没有负边:优先 Dijkstra
  • 有负边 / 需要判负环:Bellman-Ford 或 SPFA

Bellman-Ford 的一个常见变形

有时题目要求:

  • 最多经过 k 条边的最短路

这种题 Bellman-Ford 很适合做,因为“第 i 轮表示最多经过 i 条边”。

但这时要注意:
需要用一个 备份数组,避免同一轮中新更新的信息继续被用到。

模板如下:

#include <bits/stdc++.h>
using namespace std;

const int N = 505;
const long long INF = 1e18;

struct Edge {
    int u, v;
    long long w;
} e[10005];

long long dist[N], backup[N];

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, m, k, s;
    cin >> n >> m >> k >> s;
    for (int i = 1; i <= m; i++) {
        cin >> e[i].u >> e[i].v >> e[i].w;
    }

    for (int i = 1; i <= n; i++) dist[i] = INF;
    dist[s] = 0;

    for (int i = 1; i <= k; i++) {
        for (int j = 1; j <= n; j++) backup[j] = dist[j];
        for (int j = 1; j <= m; j++) {
            int u = e[j].u, v = e[j].v;
            long long w = e[j].w;
            if (backup[u] != INF) {
                dist[v] = min(dist[v], backup[u] + w);
            }
        }
    }

    for (int i = 1; i <= n; i++) {
        if (dist[i] == INF) cout << "INF ";
        else cout << dist[i] << " ";
    }
    cout << '\n';

    return 0;
}

竞赛里要注意的坑

1. INF 要开大

一般用:

const long long INF = 1e18;

不要随便开 1e9,边权和路径长度可能很大。


2. 判负环只判“从源点可达”的负环

因为代码里写了:

if (dist[u] != INF && ...)

所以只有从 s 能走到的负环才会被检测到。
这通常正是单源最短路题目要的。

如果题目要判图中 任意位置 是否存在负环,常见做法是:

  • 建一个超级源点,向所有点连权值 0 的边
  • 再跑 Bellman-Ford / SPFA 判负环

3. “恰好 k 条边” 和 “最多 k 条边” 不一样

Bellman-Ford 分层意义天然对应“最多”。
如果题目问“恰好”,需要额外设计状态。


一句话总结

Bellman-Ford 本质上是:

通过反复枚举所有边,逐步扩大允许经过的边数,从而求出单源最短路;同时可借助第 n 轮是否还能松弛来判断负环。

Updated: