探秘SPFA——强大的单源最短路径算法

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了探秘SPFA——强大的单源最短路径算法相关的知识,希望对你有一定的参考价值。

基于上次发blog,有位朋友让我多写些基本概念,就利用这次详解伟大的SPFA算法来谈。以下是百科上的算法简介,很清楚,看一遍再继续对理解程序很有帮助!(当然后面我也会解释)

SPFA(Shortest Path Faster Algorithm)(队列优化)算法是求单源最短路径的一种算法,它还有一个重要的功能是判负环(在差分约束系统中会得以体现),在Bellman-ford算法的基础上加上一个队列优化,减少了冗余的松弛操作,是一种高效的最短路算法。

求单源最短路的SPFA算法的全称是:Shortest Path Faster Algorithm,是西南交通大学段凡丁于1994年发表的(中国人的算法就是牛)。从名字我们就可以看出,这种算法在效率上一定有过人之处。很多时候,给定的图存在负权边,这时类似Dijkstra算法等便没有了用武之地,而Bellman-Ford算法的复杂度又过高,SPFA算法便派上用场了。简洁起见,我们约定加权有向图G不存在负权回路,即最短路径一定存在。如果某个点进入队列的次数超过N次则存在负环(SPFA无法处理带负环的图)。当然,我们可以在执行该算法前做一次拓扑排序,以判断是否存在负权回路,但这不是我们讨论的重点。我们用数组d记录每个结点的最短路径估计值,而且用邻接表来存储图G。我们采取的方法是动态逼近法:设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。

定理:只要最短路径存在,上述SPFA算法必定能求出最小值。证明:每次将点放入队尾,都是经过松弛操作达到的。换言之,每次的优化将会有某个点v的最短路径估计值d[v]变小。所以算法的执行会使d越来越小。由于我们假定图中不存在负权回路,所以每个结点都有最短路径值。因此,算法不会无限执行下去,随着d值的逐渐变小,直到到达最短路径值时,算法结束,这时的最短路径估计值就是对应结点的最短路径值。
期望时间复杂度:O(me), 其中m为所有顶点进队的平均次数,可以证明m一般小于等于2n:“算法编程后实际运算情况表明m一般没有超过2n.事实上顶点入队次数m是一个不容易事先分析出来的数,但它确是一个随图的不同而略有不同的常数.所谓常数,就是与e无关,与n也无关,仅与边的权值分布有关.一旦图确定,权值确定,原点确定,m就是一个确定的常数.所以SPFA算法复杂度为O(e).证毕."(SPFA的论文)不过,这个证明是非常不严谨甚至错误的,事实上在bellman算法的论文中已有这方面的内容,所以国际上一般不承认SPFA算法。
对SPFA的一个很直观的理解就是由无权图的BFS转化而来。在无权图中,BFS首先到达的顶点所经历的路径一定是最短路(也就是经过的最少顶点数),所以此时利用数组记录节点访问可以使每个顶点只进队一次,但在带权图中,最先到达的顶点所计算出来的路径不一定是最短路。一个解决方法是放弃数组,此时所需时间自然就是指数级的,所以我们不能放弃数组,而是在处理一个已经在队列中且当前所得的路径比原来更好的顶点时,直接更新最优解。
SPFA算法有两个优化策略SLF和LLL——SLF:Small Label First 策略,设要加入的节点是j,队首元素为i,若dist(j)<dist(i),则将j插入队首,否则插入队尾; LLL:Large Label Last 策略,设队首元素为i,队列中所有dist值的平均值为x,若dist(i)>x则将i插入到队尾,查找下一元素,直到找到某一i使得dist(i)<=x,则将i出队进行松弛操作。 SLF 可使速度提高 15 ~ 20%;SLF + LLL 可提高约 50%。 在实际的应用中SPFA的算法时间效率不是很稳定,为了避免最坏情况的出现,通常使用效率更加稳定的Dijkstra算法

顺便解释一下“松弛”:松弛操作是指对于每个顶点v∈V,都设置一个属性d[v],用来描述从源点s到v的最短路径上权值的上界,称为最短路径估计(shortest-path estimate)。                                                                                                           ————摘自《百度百科》

它的定义在上面第一句话解释的不能再简洁了,理解上述就好了。

至少我认为,SPFA算法是所有单源最短路算法中最实用的一种。(大佬们可以有其他想法,在此仅表示本人观点)

还是用一道求最短路径的模板题来解释:

题目描述

【题意】 给出一个图,起始点是1,结束点是N,边是双向的。求点1到点N的最短距离。哈哈,这就是标准的最短路径问题。
【输入格式】 第一行为两个整数N(1≤N≤10000)和M(0≤M≤200000)。N表示图中点的数目,M表示图中边的数目。 下来M行,每行三个整数x,y,c表示点x到点y之间存在一条边长度为c。(x≠y,1≤c≤10000) 【输出格式】 输出一行,一个整数,即为点1到点N的最短距离。 如果点1和点N不联通则输出-1。
【样例1输入】 2 1 1 2 3 【样例1输出】 3
【样例2输入】 3 3 1 2 5 2 3 5 3 1 2 【样例2输出】 2
【样例3输入】 6 9 1 2 7 1 3 9 1 5 14 2 3 10 2 4 15 3 4 11 3 5 2 4 6 6 5 6 9 【样例3输出】 20
【数据规模】 30%:1<=n<=100 50%:1<=n<=1000 100%:1<=n<=10000
故事:如何所有点(包括终点)到出发点的距离最短(最近)。 1、给出一个图有N个点,和一些有向边(无向边也行,多建立一个反向边就是) 2、一开始出发点到出发点的距离为0,其它点到出发点的距离为无穷大。 3、核心思路:其他点都在迫切的想知道自己到出发点的距离,并且他们都想自己的好朋友能更近一点到出发点(更新自己的好朋友到出发点的距离) 4、核心思路:一个点什么时候能更新自己好朋友到出发点的距离呢?当自己到出发点的距离变得更短的时候。 5、核心思路:我们建一个队列q,让能更新别人的点站到q里面。然后让q中的点一个一个出来更新。 6、最后没人出来更新了,就结束,表示所有点到出发点的距离都是最短了。

 我用的是模拟链表存图,你用其他的邻接矩阵也可以(要看点的规模,一般太大用邻接矩阵很划不来)

来吧,不废话,上代码:

#include<iostream>
#include<algorithm>
#include<cstring>
#include<queue>
using namespace std;
const int N = 10005, M = 200005, oo = 0x3fffffff; //N表示最大点的个数 ,M表示最大边的个数 ,oo是无穷大
struct Edge{
    int to,wei,next; //邻接链表的套路,to邻接顶点,wei表边的权重,next表链表指针
};
Edge edge[M]; //储存边的信息
int n,m,source,head[N],x,y,c,en(0),dist[N]; //dist【i】表示起点到 i的最短距离 
bool inq[N];
queue<int> q;
 
void Addedge(int x,int y,int c) //存图 ,x到y有一条权重为c的边 
{
    edge[en].to=y;
    edge[en].wei=c;
    edge[en].next=head[x];
    head[x]=en++;
     
}
 
void init()
{
    cin>>n>>m;
    memset(head,-1,sizeof(head)); //清零,作为每个点dfs的终止标志
    fill(inq,inq+n+1,false); //一开始都不在队列中
    while (m--)
    {
        cin>>x>>y>>c;
        Addedge(x,y,c);
        Addedge(y,x,c);
    }
    fill(dist,dist+n+1,oo); //先初始化为无穷大
    source=1; //起点
     
}
 
void spfa() //这是套路
{
    q.push(source);
    dist[source]=0;
    inq[source]=true;
    while (!q.empty())
    {
        int u=q.front();
        q.pop();
        inq[u]=false;
        for (int p=head[u];p!=-1;p=edge[p].next) //遍历整张图
        {
            int v=edge[p].to;
            if (dist[v]>dist[u]+edge[p].wei) //如果到v的距离大于到u再加上u到v的距离,就更新
            {
                dist[v]=dist[u]+edge[p].wei;
                if (inq[v]!=true)
                {
                    q.push(v);
                    inq[v]=true; //指标记,防止重复进入,否则队列没有意义
                }
            }
        }
         
    }
     
}
 
void output()
{
    cout<<dist[n]<<endl; //输出到终点的距离就行了,其实你想输出到哪点的最短距离都可以
}
 
int main()
{
    init();//输入存图
    spfa();//算法核心
    output();//输出答案
     
    return 0;
}

代码很简洁,希望大家理解!

了解SPFA是十分有用的,如果认为自己掌握的不错,就可以去做[NOIP提高组2009]最优贸易。提示:正反两遍SPFA。

题解附上,最好先不看:

 

 
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
using namespace std;
const int N = 100005, M = 500005;
struct Edge{
    int to,next;
};
Edge edge[4*M];
int n,m,en(0),price[N],head1[N],head2[N],buy[N],sell[N];
bool inq[N];
queue<int> q;
void insert(int head[],int x,int y){
    edge[en].to = y;
    edge[en].next = head[x];
    head[x] = en++;
}
 
void init(){
    //freopen("trade.in","r",stdin);
    scanf("%d%d",&n,&m);
    for (int i=1;i<=n;i++) scanf("%d",&price[i]);
    memset(head1,-1,sizeof(head1));
    memset(head2,-1,sizeof(head2));
    for (int i=0,x,y,z;i<m;i++){
        scanf("%d%d%d",&x,&y,&z);
        insert(head1,x,y);
        insert(head2,y,x);
        if (z==2){
            insert(head1,y,x);
            insert(head2,x,y);
        }
    }  
}
 
void spfa1(){
    int u,v;
    memset(buy,0x3f,sizeof(buy));
    memset(inq,0,sizeof(inq));
    buy[1] = price[1];
    q.push(1) ;
    inq[1] = true;
    while (!q.empty() ){
        u = q.front() ;
        q.pop();
        inq[u] = false;    
        for (int p=head1[u]; p!=-1; p=edge[p].next ){
            v = edge[p].to ;
            if (min(buy[u],price[v])<buy[v]){
                buy[v] = min(buy[u],price[v]);
                if (!inq[v]){
                    q.push(v);
                    inq[v] = true;
                }
            }
        }
    }
}
 
void spfa2(){
    int u,v;
    memset(sell,0,sizeof(sell));
    memset(inq,0,sizeof(inq));
    sell[n] = price[n];
    q.push(n);
    inq[n] = true;
    while (!q.empty() ){
        u = q.front();
        q.pop();
        inq[u] = false;
        for (int p=head2[u]; p!=-1; p=edge[p].next ){
            v = edge[p].to;
            if (max(sell[u],price[v])>sell[v]){
                sell[v] = max(sell[u],price[v]);
                if (!inq[v]){
                    q.push(v) ;
                    inq[v] = true;                 
                }
            }
        }
    }
}
 
int main(){
    init();
    spfa1();
    spfa2();
    int ans=0;
    for (int i=1;i<=n;i++)
        ans = max(ans,sell[i]-buy[i]);
    //freopen("trade.out","w",stdout);
    printf("%d\n",ans);
    return 0;
}

 

 

以上是关于探秘SPFA——强大的单源最短路径算法的主要内容,如果未能解决你的问题,请参考以下文章

用小根堆实现dijkstra,求图的单源最短路径

单源最短路径快速算法(spfa)的python3.x实现

单源最短路径:SPFA 算法

Bellman_Ford和SPFA:带负边权的单源最短路算法

数据结构—— 图:最短路径问题

最短路总结