NeFut Logo NeFut
EN 管理员登录

深入理解单源最短路径算法:Dijkstra与SPFA的全面对比

发布于:2026-05-27 09:17 最后更新:2026-06-06 13:04
#algorithm #Dijkstra #SPFA

核心逻辑与数学原理

单源最短路径(Single Source Shortest Path, SSSP)问题的本质是在权值图 $G=(V, E)$ 中,寻找从特定源点 $s$ 到其余所有节点 $v \in V$ 的最小边权值和路径。在 NOIP 体系下,Dijkstra 堆优化算法与 SPFA 算法代表了两种截然不同的拓扑逼近范式。

1. 贪心范式:Dijkstra 算法

Dijkstra 算法的核心是基于拓扑蓝白点集合的贪心扩张。

2. 动态逼近范式:SPFA 算法

SPFA(Shortest Path Faster Algorithm)本质上是带队列优化的 Bellman-Ford 算法,属于动态规划的迭代逼近。


算法推导与状态设计

定义 $dist[u]$ 为源点 $s$ 到节点 $u$ 的当前最短路径估计值。

1. 三角形不等式与松弛操作(Relaxation)

对于任意边 $(u, v) \in E$,权值为 $w$。松弛操作是整个最短路径算法的核心状态转移方程:

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

物理含义:判断从 $s$ 绕行 $u$ 到达 $v$ 的路径,是否比当前已知的直达或从其他途径到达 $v$ 的路径更短。

2. Dijkstra 堆优化状态设计

优先队列中存储的状态为二元组 $(d, u)$,其中 $u$ 为节点编号,$d$ 为当前的 $dist[u]$。 二叉堆顶元素满足:

$$u_{\text{next}} = \arg\min_{v \in T} (dist[v])$$

由于 C++ std::priority_queue 默认是大根堆,在工程实现时必须重载结构体比较运算符,将其改造为小根堆。


C++ 标准源码(Dijkstra 堆优化标准工业模板)

#include <iostream>
#include <vector>
#include <queue>
#include <algorithm>

const long long INF = 0x3f3f3f3f3f3f3f3fLL; // 使用足够大的长整型防止溢出
const int MAXN = 100005;

struct Edge {
    int to;
    long long weight;
};

struct HeapNode {
    long long d;
    int u;
    // 致命低级错误防范:必须严格实现小根堆比较,否则大根堆会导致复杂度退化为暴力
    bool operator>(const HeapNode& rhs) const {
        return d > rhs.d;
    }
};

std::vector<Edge> adj[MAXN];
long long dist[MAXN];
bool vis[MAXN]; // 标记是否已加入熟化集 S

void dijkstra(int s, int n) {
    // 状态初始化
    for (int i = 1; i <= n; ++i) {
        dist[i] = INF;
        vis[i] = false;
    }

    // 声明小根堆
    std::priority_queue<HeapNode, std::vector<HeapNode>, std::greater<HeapNode> > pq;

dist[s] = 0;
    pq.push(HeapNode{0, s});

    while (!pq.empty()) {
        HeapNode current = pq.top();
        pq.pop();

        int u = current.u;

        // 关键踩坑点:堆中可能存在旧的、更长距离的冗余节点,必须直接跳过
        if (vis[u]) continue; 
        vis[u] = true; // 正式并入熟化集 S

        for (size_t i = 0; i < adj[u].size(); ++i) {
            int v = adj[u][i].to;
            long long w = adj[u][i].weight;

            // 三角形不等式状态转移
            if (dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                pq.push(HeapNode{dist[v], v});
            }
        }
    }
}

int main() {
    std::ios_base::sync_with_stdio(false);
    std::cin.tie(NULL);

    int n, m, s;
    if (!(std::cin >> n >> m >> s)) return 0;

    for (int i = 0; i < m; ++i) {
        int u, v;
        long long w;
        std::cin >> u >> v >> w;
        adj[u].push_back(Edge{v, w}); // 若为无向图需双向 push_back
    }

    dijkstra(s, n);

    for (int i = 1; i <= n; ++i) {
        if (dist[i] == INF) {
            std::cout << 2147483647 << " "; // 对应部分真题要求的未到达输出值
        } else {
            std::cout << dist[i] << " ";
        }
    }
    std::cout << "\n";

    return 0;
}

NOIP 实战避坑指南

  1. 无脑迷信 SPFA 导致赛场直接被卡死: 部分选手由于习惯 SPFA 代码的简短或贪图其能处理负权的特性,在没有任何负权边的常规题中盲目选用 SPFA。近年来的 NOIP/省选评测中,凡是非负权单源最短路径题,出题人必定构造卡 SPFA 的特殊恶性数据。一旦选用 SPFA 相当于将 $O((V+E)\log V)$ 赌注押在 $O(VE)$ 的最坏结局上,直接导致 TLE 丢失 60% 以上的分数。在无负权边时,有且仅能使用 Dijkstra 堆优化
  2. 堆优化中漏判 if (vis[u]) continue; 导致时间暴崩: 线性的松弛操作会导致同一个节点 $u$ 被多次更新并重复压入优先队列。如果不做 if (vis[u]) continue; 的懒惰删除判定,在面对稠密图时,出队节点会带有大量过期的脏数据并重新执行出边的 for 循环遍历,这会让优先队列的实际规模膨胀至 $O(E)$,算法时间复杂度直接退化,达不到预期的对数级优化效果。

经典 NOIP/洛谷 真题

1. 洛谷 P4779 【模板】单源最短路径(标准版)

2. 洛谷 P3003 [USACO10MAR] Grass Traveling G

原文链接: local://10.1

[h] 返回首页