在图论的世界里,最短路径问题是最经典、最实用的课题之一。从导航软件规划路线,到网络路由协议寻找最佳路径,再到社交网络中的“六度分隔”——最短路径算法无处不在。而在算法竞赛中,最短路更是必考内容,没有之一。
C++ 作为竞赛主力语言,实现这四大算法时各有讲究:有的用优先队列,有的用队列,有的三重循环暴力松弛。本文将系统对比 Dijkstra、Bellman‑Ford、SPFA、Floyd‑Warshall 四种算法的核心思想、复杂度、适用场景和 C++ 实现要点,帮你做到“什么情况用什么算法,心里有数”。
全文约 2000 字,建议先收藏,需要时拿出来对照。
一、算法速览表(先看结论)
下面逐一展开,重点讲 C++ 实现细节和易错点。
二、Dijkstra 算法:非负权图的首选
原理与特点
Dijkstra 算法的核心思想是贪心:每次找出当前距离起点最近的“未确定”顶点,标记它为已确定,然后用它去松弛它的所有邻居。这一过程要求所有边权非负,否则已经确定的顶点可能在后来的负边下变得更短。
经典实现:O(V²) 邻接矩阵
适合稠密图(V ≤ 2000 左右)。
const int INF = 0x3f3f3f3f ;
int g[ V] [ V] ;
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)。
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} ) ;
}
}
}
}
关键点 :
三、Bellman‑Ford 算法:能处理负权的通用方案
原理
对每条边进行 V‑1 轮松弛操作。其理论基础是:在一个没有负环的图中,从源点到任意顶点的最短路径最多包含 V‑1 条边。每轮松弛至少确定一层距离。
C++ 实现(基于边集数组)
struct Edge { int u, v, w; } ;
vector< Edge> edges;
int dist[ V] ;
bool bellmanFord ( int s) {
fill ( dist, dist + V, INF) ;
dist[ s] = 0 ;
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 ;
}
for ( auto & e : edges) {
if ( dist[ e. u] != INF && dist[ e. u] + e. w < dist[ e. v] )
return false ;
}
return true ;
}
时间复杂度 O(V·E),在稠密图中较慢(V=1000,E=10⁵ 时可能 1e8 运算,勉强可接受)。
优点 :简单、能处理负权边、能检测负环。 缺点 :慢,很少单独用于正权图。
四、SPFA:Bellman‑Ford 的队列优化版
SPFA(Shortest Path Faster Algorithm)维护一个队列,只对“距离被更新过的节点”进行松弛。在随机图上表现优异,平均 O(E),但可以被构造数据卡到 O(V·E)。
C++ 实现
vector< vector< PII>> adj;
int dist[ V] , cnt[ V] ;
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)
return false ;
}
}
}
}
return true ;
}
使用场景 :正权稀疏图也可以用,但容易被卡。近年来竞赛中 SPFA 已不再是主流(很多题目会构造数据使 SPFA 退化),推荐在 负权图但无负环 且图不太大时使用,或者用于检测负环(比 Bellman‑Ford 稍快)。
注意 :判断负环的条件是 cnt[v] >= V(有 V 个节点时,最短路最多 V-1 条边,入队超过 V 次说明存在负环)。
五、Floyd‑Warshall:全源最短路的暴力美学
原理
动态规划: d[k][i][j] 表示只经过前 k 个节点时 i 到 j 的最短距离。空间优化后变成二维数组,三重循环。
int d[ V] [ V] ;
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 + 优先队列,绝对主流。
存在负权边,但无负环 :
需要检测负环 :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++ 实现中的常见陷阱与优化技巧
INF 的选择 :常用 0x3f3f3f3f(约 1e9),两个相加不会溢出 32 位 int。用 memset(dist, 0x3f, sizeof dist) 可以批量赋值。
重边的处理 :邻接矩阵读取时取 min;邻接表保留所有边,算法自动处理。
大图的存储 :永远用邻接表 vector<vector<pair<int,int>>>,不要用邻接矩阵。
Dijkstra 堆优化中的 visited 数组 :可以不用 vis,而是通过判断 dist[u] < d 来跳过旧记录(上面代码已体现)。这比维护 vis 更简洁。
SPFA 的判负环 :除了入队次数计数,还可以判断“最短路径边数”是否超过 V-1,原理相同。
Floyd 的循环顺序 : k 必须在最外层,否则错误。这体现了动态规划的阶段划分。
八、实战建议:从模板到灵活运用
掌握这四个算法后,你应该能做到:
看到“边权为正,求 1 到 n 的最短路” → 秒写 Dijkstra。
看到“边权可能为负,求最短路或判断负环” → 先想 SPFA,但注意数据强度,必要时用 Bellman‑Ford 保底。
看到“多组询问任意两点最短路,且点数 ≤ 500” → Floyd。
看到“差分约束系统” → 转化为最短路,用 Bellman‑Ford 或 SPFA 判断负环。
最后,不要满足于背模板。尝试修改 Dijkstra 来输出路径(记录前驱节点);尝试用 Floyd 求传递闭包(将边权视为布尔值);尝试结合二分答案,用最短路作为 check 函数。这些能力才是从“会写算法”到“会用算法”的升华。
最短路径的四大算法,如同武侠小说中的四大兵器:Dijkstra 是凌厉的长剑,Bellman‑Ford 是厚重的重剑,SPFA 是灵动的软剑,Floyd 是暗器般的飞镖。选对兵器,才能在算法的江湖中快意恩仇。
希望这篇文章帮你理清了思路。下一题,你的代码将更快、更稳、更优雅。