您当前的位置:首页 > 算法研究

C++图论专题:最短路径四大算法对比

时间:2026-05-03 16:49:46  来源:  作者:

在图论的世界里,最短路径问题是最经典、最实用的课题之一。从导航软件规划路线,到网络路由协议寻找最佳路径,再到社交网络中的“六度分隔”——最短路径算法无处不在。而在算法竞赛中,最短路更是必考内容,没有之一。

C++ 作为竞赛主力语言,实现这四大算法时各有讲究:有的用优先队列,有的用队列,有的三重循环暴力松弛。本文将系统对比 Dijkstra、Bellman‑Ford、SPFA、Floyd‑Warshall 四种算法的核心思想、复杂度、适用场景和 C++ 实现要点,帮你做到“什么情况用什么算法,心里有数”。

全文约 2000 字,建议先收藏,需要时拿出来对照。

一、算法速览表(先看结论)

 
 
算法 思想 时间复杂度 最短路类型 能否处理负权边 能否检测负环
Dijkstra 贪心 + 松弛 O((V+E) log V) 或 O(V²) 单源 ❌ 不能(非负权)
Bellman‑Ford 动态规划 + 松弛 O(V·E) 单源 ✅ 能
SPFA 队列优化的 Bellman‑Ford 平均 O(E),最坏 O(V·E) 单源 ✅ 能
Floyd‑Warshall 动态规划 (三重循环) O(V³) 全源 ✅ 能(无负环)

下面逐一展开,重点讲 C++ 实现细节和易错点。

二、Dijkstra 算法:非负权图的首选

原理与特点

Dijkstra 算法的核心思想是贪心:每次找出当前距离起点最近的“未确定”顶点,标记它为已确定,然后用它去松弛它的所有邻居。这一过程要求所有边权非负,否则已经确定的顶点可能在后来的负边下变得更短。

经典实现:O(V²) 邻接矩阵

适合稠密图(V ≤ 2000 左右)。

cpp
const int INF = 0x3f3f3f3f;
int g[V][V];   // 邻接矩阵,不直接相连的设为 INF
int dist[V];
bool vis[V];

void dijkstra(int s) {
    memset(dist, 0x3f, sizeof dist);
    memset(vis, 0, sizeof vis);
    dist[s] = 0;
    for (int i = 0; i < V; ++i) {
        int u = -1, minD = INF;
        for (int j = 0; j < V; ++j)
            if (!vis[j] && dist[j] < minD) {
                minD = dist[j];
                u = j;
            }
        if (u == -1) break;
        vis[u] = true;
        for (int v = 0; v < V; ++v)
            if (!vis[v] && g[u][v] != INF && dist[u] + g[u][v] < dist[v])
                dist[v] = dist[u] + g[u][v];
    }
}

优先队列优化:O((V+E) log V)

适用于稀疏图(V 较大,比如 1e5)。

cpp
using PII = pair<int, int>; // (距离, 节点)
vector<vector<PII>> adj;    // 邻接表
priority_queue<PII, vector<PII>, greater<PII>> pq;

void dijkstra(int s) {
    vector<int> dist(V, INF);
    dist[s] = 0;
    pq.push({0, s});
    while (!pq.empty()) {
        auto [d, u] = pq.top(); pq.pop();
        if (d != dist[u]) continue;   // 跳过旧版本
        for (auto [v, w] : adj[u]) {
            if (dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                pq.push({dist[v], v});
            }
        }
    }
}

关键点

  • 使用 greater 得到小根堆。

  • 从堆中取出的距离必须与当前 dist[u] 比较,否则可能拿到已经更新过的旧记录。

  • 不能处理负权边:一旦负权存在,贪心性质被破坏。

三、Bellman‑Ford 算法:能处理负权的通用方案

原理

对每条边进行 V‑1 轮松弛操作。其理论基础是:在一个没有负环的图中,从源点到任意顶点的最短路径最多包含 V‑1 条边。每轮松弛至少确定一层距离。

C++ 实现(基于边集数组)

cpp
struct Edge { int u, v, w; };
vector<Edge> edges;
int dist[V];

bool bellmanFord(int s) {
    fill(dist, dist + V, INF);
    dist[s] = 0;
    // 进行 V-1 轮松弛
    for (int i = 1; i <= V - 1; ++i) {
        bool updated = false;
        for (auto &e : edges) {
            if (dist[e.u] != INF && dist[e.u] + e.w < dist[e.v]) {
                dist[e.v] = dist[e.u] + e.w;
                updated = true;
            }
        }
        if (!updated) break;  // 提前结束
    }
    // 第 V 轮检查负环:如果能继续松弛,则存在负环
    for (auto &e : edges) {
        if (dist[e.u] != INF && dist[e.u] + e.w < dist[e.v])
            return false;   // 存在负环
    }
    return true;            // 无负环,dist 存储最短距离
}

时间复杂度 O(V·E),在稠密图中较慢(V=1000,E=10⁵ 时可能 1e8 运算,勉强可接受)。

优点:简单、能处理负权边、能检测负环。
缺点:慢,很少单独用于正权图。

四、SPFA:Bellman‑Ford 的队列优化版

SPFA(Shortest Path Faster Algorithm)维护一个队列,只对“距离被更新过的节点”进行松弛。在随机图上表现优异,平均 O(E),但可以被构造数据卡到 O(V·E)。

C++ 实现

cpp
vector<vector<PII>> adj;
int dist[V], cnt[V];      // cnt 记录入队次数,用于检测负环
bool inq[V];              // 是否已在队列中

bool spfa(int s) {
    memset(dist, 0x3f, sizeof dist);
    memset(cnt, 0, sizeof cnt);
    memset(inq, 0, sizeof inq);
    queue<int> q;
    dist[s] = 0;
    q.push(s);
    inq[s] = true;
    while (!q.empty()) {
        int u = q.front(); q.pop();
        inq[u] = false;
        for (auto [v, w] : adj[u]) {
            if (dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                if (!inq[v]) {
                    q.push(v);
                    inq[v] = true;
                    cnt[v]++;
                    if (cnt[v] >= V)   // 一个节点入队超过 V 次,说明有负环
                        return false;
                }
            }
        }
    }
    return true;
}

使用场景:正权稀疏图也可以用,但容易被卡。近年来竞赛中 SPFA 已不再是主流(很多题目会构造数据使 SPFA 退化),推荐在负权图但无负环且图不太大时使用,或者用于检测负环(比 Bellman‑Ford 稍快)。

注意:判断负环的条件是 cnt[v] >= V(有 V 个节点时,最短路最多 V-1 条边,入队超过 V 次说明存在负环)。

五、Floyd‑Warshall:全源最短路的暴力美学

原理

动态规划:d[k][i][j] 表示只经过前 k 个节点时 i 到 j 的最短距离。空间优化后变成二维数组,三重循环。

cpp
int d[V][V];   // 初始为邻接矩阵,d[i][i]=0,不直接相连为 INF

void floyd() {
    for (int k = 0; k < V; ++k)
        for (int i = 0; i < V; ++i)
            for (int j = 0; j < V; ++j)
                if (d[i][k] != INF && d[k][j] != INF && d[i][k] + d[k][j] < d[i][j])
                    d[i][j] = d[i][k] + d[k][j];
}

复杂度 O(V³)。当 V ≤ 400 时可以接受,V=500 时 1.25e8 运算,勉强能过。
优点:实现极简,代码只有几行;能处理负权边(但不能有负环)。
检测负环:算法结束后,如果存在 d[i][i] < 0,则图中存在负环(因为从 i 到 i 的最短路为负,意味着绕了一圈权值和为负)。

六、四大算法对比与选型指南

根据图的规模选

  • V ≤ 400:Floyd 最舒服,代码简单,还能直接得到所有点对距离。

  • V 大(1e5),边权非负:Dijkstra + 优先队列,绝对主流。

  • 存在负权边,但无负环

    • 若图较稀疏(E ~ 1e5),且不怕被卡,可用 SPFA。

    • 若图较稠密或担心 SPFA 被卡,用 Bellman‑Ford(O(V·E) 可接受时)。

  • 需要检测负环:Bellman‑Ford 或 SPFA 都可以。Floyd 也能检测但成本高。

根据题目要求选

  • 单源最短路 + 非负权:毫不犹豫 Dijkstra。

  • 单源最短路 + 可能有负权:用 SPFA 或 Bellman‑Ford。

  • 多次查询不同源的最短路:如果 V 小,Floyd 预处理 O(V³),之后 O(1) 回答;如果 V 大,无法 Floyd,则每个查询跑一次 Dijkstra(假设非负权)。

  • 图含负环:需要判断时,一般用 Bellman‑Ford 或 SPFA。

一个常见误区

很多人以为“SPFA 比 Dijkstra 快”,这是错误的。在正权图上,Dijkstra + 堆是 O((V+E) log V),而 SPFA 虽然是队列,但理论上界仍然是 O(V·E),而且实际中很容易被构造数据卡到接近上界。现在绝大多数竞赛题目的正权图,出题人都会卡 SPFA。因此请养成“非负权用 Dijkstra”的习惯。

七、C++ 实现中的常见陷阱与优化技巧

  1. INF 的选择:常用 0x3f3f3f3f(约 1e9),两个相加不会溢出 32 位 int。用 memset(dist, 0x3f, sizeof dist) 可以批量赋值。

  2. 重边的处理:邻接矩阵读取时取 min;邻接表保留所有边,算法自动处理。

  3. 大图的存储:永远用邻接表 vector<vector<pair<int,int>>>,不要用邻接矩阵。

  4. Dijkstra 堆优化中的 visited 数组:可以不用 vis,而是通过判断 dist[u] < d 来跳过旧记录(上面代码已体现)。这比维护 vis 更简洁。

  5. SPFA 的判负环:除了入队次数计数,还可以判断“最短路径边数”是否超过 V-1,原理相同。

  6. Floyd 的循环顺序k 必须在最外层,否则错误。这体现了动态规划的阶段划分。

八、实战建议:从模板到灵活运用

掌握这四个算法后,你应该能做到:

  • 看到“边权为正,求 1 到 n 的最短路” → 秒写 Dijkstra。

  • 看到“边权可能为负,求最短路或判断负环” → 先想 SPFA,但注意数据强度,必要时用 Bellman‑Ford 保底。

  • 看到“多组询问任意两点最短路,且点数 ≤ 500” → Floyd。

  • 看到“差分约束系统” → 转化为最短路,用 Bellman‑Ford 或 SPFA 判断负环。

最后,不要满足于背模板。尝试修改 Dijkstra 来输出路径(记录前驱节点);尝试用 Floyd 求传递闭包(将边权视为布尔值);尝试结合二分答案,用最短路作为 check 函数。这些能力才是从“会写算法”到“会用算法”的升华。


最短路径的四大算法,如同武侠小说中的四大兵器:Dijkstra 是凌厉的长剑,Bellman‑Ford 是厚重的重剑,SPFA 是灵动的软剑,Floyd 是暗器般的飞镖。选对兵器,才能在算法的江湖中快意恩仇。

希望这篇文章帮你理清了思路。下一题,你的代码将更快、更稳、更优雅。

淮安信息学奥赛CSP J/S 培训网, 一个专注于信息学奥赛学习的网站,专注于信息学奥赛培优,助力于孩子在NOIP/NOI/CSP/IOI中取得好成绩。