写在前面:
前面我们谈到了Bellman-Ford算法,并埋下了一个伏笔。
每进行一次松弛操作,就会有一些顶点已经求得其最短路径,即这些顶点的最短路径的“估计值”变为“确定值”。此后这些顶点的最短路径的值就会一直保持不变,不再受后续松弛操作的影响。但是,每次还是会判断是否需要松弛,这里浪费了时间。
那么,要怎么优化呢?如何知道当前哪些点的最短路径发生了变化?
我们可以用一个队列来维护这些点。
队列优化Bellman-Ford
算法流程:

图解:


参考代码:【 (数组实现)邻接表构图 】
PS:数组实现邻接表可能较难理解,可以看一下这里
这里用一个book数组来记录每个顶点是否在队列中。其实也可以不用book数组,检查一个顶点是否在队列中,只需要遍历一遍整个队列就可以了,但是那样时间复杂度就相对比较高,达到,而使用book数组来记录,时间复杂度将降为
。
#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;
}
时间复杂度:
该算法的复杂度为,其中
为边数,
是个比较小的常数。不过其证明好像是错误的。(参见百度百科)
最坏情况的时间复杂度也是。
更多:

SPFA算法
最短路径快速算法(英语:Shortest Path Faster Algorithm (SPFA)),国际上一般认为是队列优化的贝尔曼-福特算法,是一个用于求解有向带权图单源最短路径的算法。这一算法被认为在随机的稀疏图上表现出色,并且适用于带有负边权的图。[1] 然而SPFA在最坏情况的时间复杂度与贝尔曼-福特算法相同,因此在非负边权的图中仍然最好使用戴克斯特拉算法。[2] SPFA算法首先在1959年由Edward F. Moore作为广度优先搜索的扩展发表[3]。相同算法在1994年由段凡丁重新发现。[4]
——《维基百科》
优化技巧:
SPFA算法的性能很大程度上取决于用于松弛其他节点的备选节点的顺序。
事实上,如果
是一个优先队列,则这个算法将极其类似于戴克斯特拉算法。然而尽管这一算法中并没有用到优先队列,仍有两种可用的技巧可以用来提升队列的质量,并且借此能够提高平均性能(但仍无法提高最坏情况下的性能)。
两种技巧通过重新调整
中元素的顺序从而使得更靠近源点的节点能够被更早地处理。因此一旦实现了这两种技巧,
将不再是一个先进先出队列,而更像一个链表或双端队列。
(想想也是,如果我们能够在一开始查找的时候,将可能是更短的路径找到,那么,最终找最短路径花的时间也就可能更少。)
——《维基百科》
-
距离小者优先 SLF(Small Label First):
原队列改为双 向 队 列
,对一个要加入队列的点u,如果小于队首元素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):
-
对每个要出队的元素u,比较
和队列中每个元素
dis的平均值。即:比较大小,也就是
与
比较大小。
-
如果
更大,那么将它弹出放到队尾,取队首元素,继续进行重复判断。
-
直至存在一个
小于平均值,将元素
出队。
//【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)算法
- 博客:SPFA算法优化—SLF和LLL优化
- 博客:SPFA 的两个优化
在学习的过程中,不断总结,虽花时间,但也不无收获。
水平有限,如有错误或建议,还望指出!!!
网友评论