美文网首页
最短路径 | 优化Bellman-Ford(SPFA)

最短路径 | 优化Bellman-Ford(SPFA)

作者: 0与1的邂逅 | 来源:发表于2019-05-25 21:56 被阅读0次

写在前面:

前面我们谈到了Bellman-Ford算法,并埋下了一个伏笔。

每进行一次松弛操作,就会有一些顶点已经求得其最短路径,即这些顶点的最短路径的“估计值”变为“确定值”。此后这些顶点的最短路径的值就会一直保持不变,不再受后续松弛操作的影响。但是,每次还是会判断是否需要松弛,这里浪费了时间。

那么,要怎么优化呢?如何知道当前哪些点的最短路径发生了变化?

我们可以用一个队列来维护这些点。

队列优化Bellman-Ford

算法流程:

图解:

参考代码:【 (数组实现)邻接表构图 】

PS:数组实现邻接表可能较难理解,可以看一下这里

这里用一个book数组来记录每个顶点是否在队列中。其实也可以不用book数组,检查一个顶点是否在队列中,只需要遍历一遍整个队列就可以了,但是那样时间复杂度就相对比较高,达到O(N),而使用book数组来记录,时间复杂度将降为O(1)

#include<iostream>
#include<cstdio>
#include<queue>
using namespace std;

const int VexNum=1e3+1;// 顶点数 
const int ArcNum=6e6;// 边数 
const int INF=0x3f3f3f3f;// 正无穷 
// first数组大小要比 n大 1,next数组大小要比 m大 1 
int first[VexNum],next[ArcNum];
// u、v、w数组大小要比 m大1 
int u[ArcNum],v[ArcNum],w[ArcNum];
int dis[VexNum];// 距离表 
int book[VexNum];// 记录哪些顶点已经在队列中 
int n,m;// n:顶点;m:边 

int main()
{
    scanf("%d%d",&n,&m);
    
    // 初始化dis数组 
    for(int i=1;i<=n;i++)dis[i]=INF;
    dis[1]=0;
    
    // 初始化book数组,0表示顶点不在队列中 
    for(int i=1;i<=n;i++)book[i]=0;
    
    // 初始化first数组,-1表示暂时没有边 
    for(int i=1;i<=n;i++)first[i]=-1;
    
    // 读入每条边 
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d%d",&u[i],&v[i],&w[i]);
        // 建立邻接表的关键
        next[i]=first[u[i]];
        first[u[i]]=i; 
    }
    
    queue<int>q;// 创建队列 
    q.push(1);// 1号顶点入队 
    book[1]=1;// 1号顶点已在队列中
    
    // 当队列不为空 
    while(!q.empty())
    {
        int k=q.front();// 取队首元素 
        // 队首元素出队列 
        q.pop();
        book[k]=0;// 出队列 
        // 扫描当前顶点所有的边 
        while(k!=-1)
        {
            // 判断是否松弛成功 
            if(dis[v[k]]>dis[u[k]]+w[k])
            {
                dis[v[k]]=dis[u[k]]+w[k];
                // 用book数组来判断顶点v[k]是否在队列中 
                if(book[v[k]]==0)
                {
                    q.push(v[k]);
                    book[v[k]]=1;// 入队列 
                }
            }
            k=next[k];
        }
    }
    for(int i=1;i<=n;i++)
    {
        printf("%d ",dis[i]);
    }
    printf("\n"); 
    return 0;
}

时间复杂度:

该算法的复杂度为O(k|M|),其中M为边数,k是个比较小的常数。不过其证明好像是错误的。(参见百度百科)

最坏情况的时间复杂度也是O(NM)


更多:

SPFA算法

最短路径快速算法(英语:Shortest Path Faster Algorithm (SPFA)),国际上一般认为是队列优化的贝尔曼-福特算法是一个用于求解有向带权图单源最短路径的算法。这一算法被认为在随机的稀疏图上表现出色,并且适用于带有负边权的图。[1] 然而SPFA在最坏情况的时间复杂度与贝尔曼-福特算法相同,因此在非负边权的图中仍然最好使用戴克斯特拉算法[2] SPFA算法首先在1959年由Edward F. Moore作为广度优先搜索的扩展发表[3]。相同算法在1994年由段凡丁重新发现。[4]
——《维基百科》

优化技巧:

SPFA算法的性能很大程度上取决于用于松弛其他节点的备选节点的顺序。

事实上,如果Q是一个优先队列,则这个算法将极其类似于戴克斯特拉算法。然而尽管这一算法中并没有用到优先队列,仍有两种可用的技巧可以用来提升队列的质量,并且借此能够提高平均性能(但仍无法提高最坏情况下的性能)

两种技巧通过重新调整Q中元素的顺序从而使得更靠近源点的节点能够被更早地处理。因此一旦实现了这两种技巧,Q将不再是一个先进先出队列,而更像一个链表或双端队列。

(想想也是,如果我们能够在一开始查找的时候,将可能是更短的路径找到,那么,最终找最短路径花的时间也就可能更少。)

——《维基百科》

  • 距离小者优先 SLF(Small Label First):
    原队列改为双 向 队 列,对一个要加入队列的点u,如果dis[u] 小于队首元素v的dis[v],那么就加入到队首,否则加入到队尾。
//【SLF】 
// 原队列改为双端队列,对一个要加入队列的点u,
// 如果dis[u] 小于队首元素v的dis[v],那么就加入到队首元素,
// 否则加入到队尾。
#include<iostream>
#include<cstdio>
#include<queue>
using namespace std;

const int VexNum=1e3+1;// 顶点数 
const int ArcNum=6e6;// 边数 
const int INF=0x3f3f3f3f;// 正无穷 
// first数组大小要比 n大 1,next数组大小要比 m大 1 
int first[VexNum],next[ArcNum];
// u、v、w数组大小要比 m大1 
int u[ArcNum],v[ArcNum],w[ArcNum];
int dis[VexNum];// 距离表 
int book[VexNum];// 记录哪些顶点已经在队列中 
int n,m;// n:顶点;m:边 

int main()
{
    scanf("%d%d",&n,&m);
    // 初始化dis数组 
    for(int i=1;i<=n;i++)dis[i]=INF;
    dis[1]=0;
    
    // 初始化book数组,0表示顶点不在队列中 
    for(int i=1;i<=n;i++)book[i]=0;
    
    // 初始化first数组,-1表示暂时没有边 
    for(int i=1;i<=n;i++)first[i]=-1;
    
    // 读入每条边 
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d%d",&u[i],&v[i],&w[i]);
        // 建立邻接表的关键
        next[i]=first[u[i]];
        first[u[i]]=i; 
    }
    
    deque<int>dq;// 创建双向队列deque
    dq.push_back(1);// 1号顶点入队尾 
    book[1]=1;// 1号顶点已在队列中
    
    // 当队列不为空 
    while(!dq.empty())
    {
        int k=dq.front();// 取队首元素 
        int temp=k;
        // 队首元素出队列 
        dq.pop_front();
        book[k]=0;
        // 扫描当前顶点所有的边 
        while(k!=-1)
        {
            // 判断是否松弛成功 
            if(dis[v[k]]>dis[u[k]]+w[k])
            {
                dis[v[k]]=dis[u[k]]+w[k];
                // 用book数组来判断顶点v[k]是否在队列中 
                if(book[v[k]]==0)
                {
                    if(dis[temp]>dis[v[k]])dq.push_front(v[k]);// 入队首
                    else dq.push_back(v[k]);// 入队尾
                    book[v[k]]=1;
                }
            }
            k=next[k];
        }
    }
    for(int i=1;i<=n;i++)
    {
        printf("%d ",dis[i]);
    }
    printf("\n"); 
    return 0;
}
  • 距离大者置后 LLL(Large Label Last):
  1. 每个要出队的元素u,比较dis[u]和队列中每个元素
    dis的平均值。即:dis[u] 与 \frac{\sum_{i=0}^{len-1}dis[j]_{j∈Q}}{len}比较大小,也就是dis[u]*len\sum_{i=0}^{len-1}dis[j]_{j∈Q}比较大小。

  2. 如果dis[u]更大,那么将它弹出放到队尾,取队首元素,继续进行重复判断。

  3. 直至存在一个dis[x]小于平均值,将元素x出队。

//【LLL】 
// 对每个要出对的元素u,比较dis[u]和队列中dis的平均值, 
// 如果dis[u]更大,那么将它弹出放到队尾,取队首元素在进行重复判断,
// 直至存在dis[x]小于平均值 
#include<iostream>
#include<cstdio>
#include<queue>
using namespace std;

const int VexNum=1e3+1;// 顶点数 
const int ArcNum=6e6;// 边数 
const int INF=0x3f3f3f3f;// 正无穷 
// first数组大小要比 n大 1,next数组大小要比 m大 1 
int first[VexNum],next[ArcNum];
// u、v、w数组大小要比 m大1 
int u[ArcNum],v[ArcNum],w[ArcNum];
int dis[VexNum];// 距离表 
int book[VexNum];// 记录哪些顶点已经在队列中 
int n,m;// n:顶点;m:边 

long long sum=0;
int len=1;

int main()
{
    scanf("%d%d",&n,&m);
    
    // 初始化dis数组 
    for(int i=1;i<=n;i++)dis[i]=INF;
    dis[1]=0;
    
    // 初始化book数组,0表示顶点不在队列中 
    for(int i=1;i<=n;i++)book[i]=0;
    
    // 初始化first数组,-1表示暂时没有边 
    for(int i=1;i<=n;i++)first[i]=-1;
    
    // 读入每条边 
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d%d",&u[i],&v[i],&w[i]);
        // 建立邻接表的关键
        next[i]=first[u[i]];
        first[u[i]]=i; 
    }
    
    queue<int>q;// 普通队列
    q.push(1);// 1号顶点入队 
    book[1]=1;// 1号顶点已在队列中
    len=1;// 对内元素个数 
    sum=0;// 对内元素总和 
    
    // 当队列不为空 
    while(!q.empty())
    {
        int k=q.front();// 取队首元素 
        while(dis[k]*len>sum)
        {
            q.pop();
            q.push(k);
            k=q.front();
        }
        q.pop();
        len--;// 出队,队内元素数-1 
        sum-=dis[k];// 出队,队内元素总和减去出队的元素 
        book[k]=0;
    
        // 扫描当前顶点所有的边 
        while(k!=-1)
        {
            // 判断是否松弛成功 
            if(dis[v[k]]>dis[u[k]]+w[k])
            {
                dis[v[k]]=dis[u[k]]+w[k];
                // 用book数组来判断顶点v[k]是否在队列中 
                if(book[v[k]]==0)
                {
                    q.push(v[k]);
                    sum+=dis[v[k]];
                    len++;
                }
            }
            k=next[k];
        }
    }
    for(int i=1;i<=n;i++)
    {
        printf("%d ",dis[i]);
    }
    printf("\n"); 
    return 0;
}
  • SLF+LLL:
    将前面两种优化方法结合起来,即可。
#include<iostream>
#include<cstdio>
#include<queue>
using namespace std;

const int VexNum=1e3+1;// 顶点数 
const int ArcNum=6e6;// 边数 
const int INF=0x3f3f3f3f;// 正无穷 
// first数组大小要比 n大 1,next数组大小要比 m大 1 
int first[VexNum],next[ArcNum];
// u、v、w数组大小要比 m大1 
int u[ArcNum],v[ArcNum],w[ArcNum];
int dis[VexNum];// 距离表 
int book[VexNum];// 记录哪些顶点已经在队列中 
int n,m;// n:顶点;m:边 
long long sum=0;
int len=1; 

int main()
{
    scanf("%d%d",&n,&m);
    
    // 初始化dis数组 
    for(int i=1;i<=n;i++)dis[i]=INF;
    dis[1]=0;
    
    // 初始化book数组,0表示顶点不在队列中 
    for(int i=1;i<=n;i++)book[i]=0;
    
    // 初始化first数组,-1表示暂时没有边 
    for(int i=1;i<=n;i++)first[i]=-1;
    
    // 读入每条边 
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d%d",&u[i],&v[i],&w[i]);
        // 建立邻接表的关键
        next[i]=first[u[i]];
        first[u[i]]=i; 
    }
     
    deque<int>dq;// 双向队列 
    dq.push_back(1);// 1号顶点入队尾 
    book[1]=1;// 1号顶点已在队列中
    sum=0;
    len=1;
    
    // 当队列不为空 
    while(!dq.empty())
    {
        int k=dq.front();// 取队首元素 
        while(dis[k]*len>sum)
        {
            dq.pop_front();// 出队首元素
            dq.push_back(k);// 将其压入队尾 
            k=dq.front();// 取队首元素 
        }
        int temp=k;
        // 队首元素出队列 
        dq.pop_front();
        len--;
        sum-=dis[k];
        book[k]=0;
        // 扫描当前顶点所有的边 
        while(k!=-1)
        {
            // 判断是否松弛成功 
            if(dis[v[k]]>dis[u[k]]+w[k])
            {
                dis[v[k]]=dis[u[k]]+w[k];
                // 用book数组来判断顶点v[k]是否在队列中 
                if(book[v[k]]==0)
                {
                    if(dis[temp]>dis[v[k]])dq.push_front(v[k]);
                    else dq.push_back(v[k]);
                    book[v[k]]=1;// 入队列 
                }
            }
            k=next[k];
        }
    }
    for(int i=1;i<=n;i++)
    {
        printf("%d ",dis[i]);
    }
    printf("\n"); 
    return 0;
}

写在最后:

参考资料:

在学习的过程中,不断总结,虽花时间,但也不无收获。

水平有限,如有错误或建议,还望指出!!!

相关文章

网友评论

      本文标题:最短路径 | 优化Bellman-Ford(SPFA)

      本文链接:https://www.haomeiwen.com/subject/hjqgzqtx.html