上一节,我们讲了求带负权边有向图最短路径的方法–bellman-ford算法。友情链接:bellman-ford算法。
上节说到,bellman算法有一个致命缺点:时间复杂度过高,达到了 O ( V E ) O(VE) O(VE)。主要是因为,有些情况下对结点的更新是不必要的。因此,bellman和ford又各自提出了改进方法–队列法。1994年,西南交大的段凡丁又重新发现了该算法,并将其命名为SPFA(shortest path fast algorithm),并通过实验发现,平均时间复杂度为 O ( E ) O(E) O(E)。但是,SPFA最坏情况的时间复杂度依然为 O ( V E ) O(VE) O(VE)。因此,对于无向图和非负权有向图,尽量使用迪杰斯特拉算法;对于有向无环图,使用拓扑排序+dp。
一、SPFA算法简介
既然是队列法,我们就先说说这个队列有什么用。这个队列是用来保存已更新的结点,并不断弹出,更新和弹出结点相邻的结点,并把已更新且不在队列中的结点再加入队列中。
和bellman-ford算法不同,前者是简单地更新所有的节点,而后者是更新上一步更新过的结点的邻接结点,类似于BFS。所以,这样就比bellman-ford节省了大量时间。但是,保存图就需要邻接表了,编码量也就略提高了一些。
算法也可以用来检测负权环。如果一个结点入队达到或超过V次(V为顶点个数),则存在负权环。证明从略。
这个简介可能比较难懂,我们下面详细说。
二、算法详解
第一步:初始化路径长度数组len,为一个很大的数(比如INT_MAX
)。并使len[s] = 0
。构造一个空队列Q,把源点S加入队列中。初始化结点入队次数数组cnt
全为0。
第二步:每次从队列中弹出一个元素u。扫描和这个元素相邻的所有边e(u, v)
,其权重为w(u, v)
。如果满足len[u] + w(u, v) < len[v]
,则更新len[v] = len[u] + w(u, v)
,并检查顶点v是否在队列Q中(至关重要),如果不在,则加入队列,并更新入队次数数组cnt[v]++
;如果某个结点入队次数达到或超过V,则输出错误信息表示有负权环。
第三步:不断重复第二步,直到队列为空为止。然后输出len数组。
所以,这里比bellman-ford算法的一个关键改进,就是只需要更新 [和刚更新过的结点相邻的结点] 。如果队列为空,则表明上次扫描再未发现任何一个可更新的结点,算法可以结束。
伪代码
def SPFA(graph, v, s): bool
{
for i in range(1, v)
{
len[i] = INT_MAX;
}
len[s] = 0;
queue q;
q.push_back(s);
while (!q.empty())
{
u = q.pop_front();
for e in graph[u] # 和u相邻的边e,包括两个属性:邻接点v和权值weight
{
if (len[u] + e.weight < len[e.v])
{
len[e.v] = len[u] + e.weight;
if not (e.v in q)
{
q.push_back(e.v);
cnt[e.v]++;
if (cnt[e.v] >= v) return false;
}
}
}
}
return true;
}
三、效率分析和改进
时间复杂度
显然,平均时间复杂度为 O ( k E ) O(kE) O(kE),其中,k为所有结点的平均入队次数。通过大量实验研究,得出在绝大多数情况下 k < = 2 k<=2 k<=2。但是,理论上最坏情况的时间复杂度是 O ( V E ) O(VE) O(VE)。可以这样简单思考:每个结点入队次数不可能超过V-1次。如果每个结点都入队V-1次,则时间复杂度为 O ( V E ) O(VE) O(VE)。至于是否存在这样的图,需要严格证明,这里可以告诉大家结论:是存在的。
空间复杂度
需要邻接表, O ( E ) O(E) O(E)。其余的辅助数组等,需要 O ( V ) O(V) O(V)。
改进
在Wikipedia-SPFA(访问需要科学的力量)上有一个改进方法,叫做距离小者优先(Small Label First(SLF))。意思是,比较更新后的 l e n [ v ] len[v] len[v]和 l e n [ Q . t o p ( ) ] len[Q.top()] len[Q.top()],并且在 l e n [ v ] len[v] len[v]较小时将v压入队列的头端。因此,上述伪代码中的第20行可以修改为:
if (!q.empty && len[e.v] < len[q.top()])
q.push_front(e.v);
else
q.push_back(e.v);
四、代码实现
题目描述:给定一个有向图,可能有负权边,求从源点S出发到所有其它点的最短路径。如果有负权环,输出一行“has negative loop!”。如果是源点本身,距离为0。如果是从S不可达的点,距离为INT_MAX
。数据保证S到任意可达的点最短距离在int范围内。
输入:多组(不超过10组)输入数据。
每组第一行三个整数,顶点数v,边数e,源点S。
1
<
=
v
<
=
3000
,
0
<
=
e
<
=
30000
,
1
<
=
S
<
=
v
1<=v <= 3000, 0<=e <= 30000, 1<=S<=v
1<=v<=3000,0<=e<=30000,1<=S<=v。接下来e行,每行三个整数,表示每条边的出发点
v
1
v1
v1,终点
v
2
v2
v2,长度
w
w
w。
1
<
=
v
1
,
v
2
<
=
v
1<=v1,v2<=v
1<=v1,v2<=v。
输出:对于每组数据输出一行,用空格隔开的v个整数,第i个整数表示S到i的最短距离。如果有负权环,输出一行“has negative loop!”(不含引号)。
时间限制:2000ms,空间限制:128MB。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <unordered_map>
#include <deque>
using namespace std;
unordered_map<int, long long> graph[1001]; // 邻接表
long long len[1001];
bool flag[1001]; // 指示顶点u是否在队列中
int cnt[1001]; // 入队次数
inline void add_edge(int v1, int v2, long long weight)
{
if (!graph[v1].count(v2) || graph[v1][v2] > weight)
graph[v1][v2] = weight;
}
int main() {
int v, e, s, i, v1, v2;
long long w;
while (scanf("%d %d %d", &v, &e, &s) != EOF)
{
while (e--)
{
scanf("%d %d %lld", &v1, &v2, &w);
add_edge(v1, v2, w);
}
for (i = 1; i <= v; i++) len[i] = (long long) INT_MAX;
len[s] = 0;
deque<int> dq; // 用deque是为了实现SLF
dq.push_back(s);
cnt[s] = 1;
while (!dq.empty())
{
s = dq.front();
dq.pop_front();
flag[s] = false; // 出队时,需要更新flag数组
for (unordered_map<int, long long>::iterator it = graph[s].begin(); it != graph[s].end(); it++)
{
int t = it->first;
if (len[s] + it->second < len[t])
{
len[t] = len[s] + it->second;
if (flag[t]) continue; // 入队时,需要更新flag和cnt数组
if (++cnt[t] >= v)
{
printf("has negative loop!");
goto label;
}
if (!dq.empty() && len[t] < len[dq.front()])
{
dq.push_front(t);
}
else dq.push_back(t);
flag[t] = true;
}
}
}
for (i = 1; i <= v; i++)
{
printf("%lld ", len[i]);
graph[i].clear();
}
memset(flag, false, sizeof(flag));
memset(cnt, 0, sizeof(cnt));
label: printf("\n");
}
return 0;
}