5.2.4 SPFA (Bellman-Ford)
适用条件:单源、包含负边权、判断是否存在负环。
[注]:在没有负边权的正权图里,优先使用 Dijkstra,因为 SPFA 容易被特殊构造的数据卡成
Bellman-Ford 算法是单源最短路径算法,求一个起点
有个现实的模型,即”问路”。每个十字路口站着一个警察;在某个路口,路人问一个警察:”怎么到
在这个模型中,能体现 Bellman-Ford 算法思想的是:警察不需要知道到
但是,纯粹的 Bellman-Ford 太慢了,因为每次连那些根本没被更新过的点也要跟着广播,于是,有了 SPFA,这可以说是用队列优化的 Bellman-Ford。这个改进方法在于,每轮计算只需要更新上一轮有变化的那些点的邻居,而那些没有变化的点不需要更新他们的邻居,而这种改进恰好用队列处理非常合适。SPFA 在一般情况下和 Dijkstra 算法一样快,甚至还要更好,但是在最差的情况下复杂度仍然为
必备概念
先记下四个核心结构:
dist[N]:存储源点到每个点的最短距离,初始化为 inf。vis[N]:表示这个点是否已经在队列里排队,若已入队,则没必要重复入队。cnt[N]:记录每个点被拉入队列的次数。如果某个点入队超过次,说明图里有负权环。 queue<int> q:用来存放那些刚刚被更新了距离,需要去通知邻居的点。
下面让我们代入一个场景: 假设你在玩一个游戏,从起点 A 走到各个房间。A-B 花费 2,A-C 花费 5。但有一个特殊的传送门 B-C,由于是倒贴奖励,花费为 -4。
沙盘推演:
- 第一步:发车 起点 A 的距离
dist[A] = 0。A 被扔进队列q里。 - 第二步:A 广播 队列弹出 A。A 说:”我到起点是 0,邻居们,你们加上我的距离看看有没有变短?” B 发现
0 + 2 < inf,于是dist[B] = 2。B 被扔进队列。 C 发现0 + 5 < inf,于是dist[C] = 5。C 被扔进队列。 此时队列:[B, C]。 - 第三步:B 广播(触发负权推翻) 队列弹出 B。B 说:”我现在到起点是 2,邻居们快看看!” B 通向 C 的路是 -4。C 一听,计算了一下:
dist[B] + (-4) = 2 - 4 = -2。 原来dist[C]是 5,现在变成了 -2! C 惨遭推翻,距离被更新。 因为 C 的底牌变了,它必须重新去通知它的邻居,而此时 C 恰好还在队列里等候,所以不需要重复入队。 - 第四步:C 广播 队列弹出 C。C 发现自己没有别的邻居了。 队列为空,算法结束。 最终距离:
dist[B] = 2,dist[C] = -2。
代码解析
初始化
dist设置为inf,把起点放进队列,并标记为正在排队。1
2
3
4
5
6
7vector<int> dist(n+1,inf);
vector<bool> vis(n+1,false);
queue<int> q;
dist[s] = 0;
q.push(s);
vis[s] = true;核心循环
每次从队头拿出一个点
u,并且把vis[u]改回false,表示出队。1
2
3
4
5while(!q.empty()){
int u = q.front();
q.pop();
vis[u] = false;
}对于
u的每一个邻居节点,更新距离。1
2
3
4
5
6
7
8
9
10
11
12
13while(!q.empty()){
int u = q.front();
q.pop();
vis[u] = false;
for(auto& ei : e[u]){
int v = ei.v,w = ei.w;
if(dist[v] > dist[u]+w){
dist[v] = dist[u] + w;
}
}
}同时,距离变短后,如果
v不在队列里,赶紧把它拉进去排队通知别人1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17while(!q.empty()){
int u = q.front();
q.pop();
vis[u] = false;
for(auto& ei : e[u]){
int v = ei.v,w = ei.w;
if(dist[v] > dist[u]+w){
dist[v] = dist[u] + w;
if(!vis[v]){
q.push(v);
vis[v] = true;
}
}
}
}除此之外,还要再加一条防死循环的逻辑。和溢位。比方说图中有一个环,跑一圈的总权重是负数,那 SPFA 就会永远在里面绕圈,距离会趋近于负无穷。为此,我们可以维护一个
cnt数组,一个点只要入队次数,直接判定存在负环。 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25vector<int> cnt(n+1,0);
while(!q.empty()){
int u = q.front();
q.pop();
vis[u] = false;
for(auto& ei : e[u]){
int v = ei.v,w = ei.w;
if(dist[v] > dist[u]+w){
dist[v] = dist[u] + w;
if(!vis[v]){
q.push(v);
vis[v] = true;
cnt[v]++;
if(cnt[v] >= n){
return false;
}
}
}
}
}
return true;
综合代码
1 | const int N = 5005; |
例题
1. P3371 【模板】单源最短路径(弱化版)
Ques
题目描述
如题,给出一个有向图,请输出从某一点出发到所有点的最短路径长度。
输入格式
第一行为三个正整数
第二行起
输出格式
输出一行
输入 #1
1 | 4 6 1 |
输出 #1
1 | 0 2 4 3 |
说明/提示
【数据范围】
对于
对于
对于
对于
Update 2022/07/29:两个点之间可能有多条边,敬请注意。
对于真正
Ans
1 | struct node{ |
相关文章
[[5-2-3-Dijkstra]] [[5-2-6-Floyd]] [[5-3-2-MST]]

