漫话最短路径(三)--SPFA算法

上一节,我们讲了求带负权边有向图最短路径的方法–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 &lt; = 2 k&lt;=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 &lt; = v &lt; = 3000 , 0 &lt; = e &lt; = 30000 , 1 &lt; = S &lt; = v 1&lt;=v &lt;= 3000, 0&lt;=e &lt;= 30000, 1&lt;=S&lt;=v 1<=v<=3000,0<=e<=30000,1<=S<=v。接下来e行,每行三个整数,表示每条边的出发点 v 1 v1 v1,终点 v 2 v2 v2,长度 w w w 1 &lt; = v 1 , v 2 &lt; = v 1&lt;=v1,v2&lt;=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;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值