参考书籍:算法竞赛入门到进阶 罗勇军
SPFA简介
用队列处理Bellman-Ford算法可以很好地优化,这种方法叫做SPFA。SPFA的效率很高,在算法竞赛中的应用很广泛。
Bellman-Ford算法有很多低效或无效的操作。分析Bellman-Ford算法,其核心部分是在每一轮操作中更新所有结点到起点s的最短距离。根据前面的讨论可知,计算和调整一个结点u到s的最短距离后,如果紧接着调整u的邻居结点,这些邻居肯定有新的计算结果;而如果漫无目的地计算不与u相邻的结点,很可能毫无变化,所以这些操作是低效的。
因此,在计算结点u之后,下一步只计算和调整它的邻居,这样能加快收敛的过程。这些步骤可以用队列进行操作,这就是SPFA
SPFA很像BFS;
①起点s入队,计算它所有邻居到s的最短距离(当前最短距离,不是全局最短距离。在下文中,把计算一个结点到起点s的最短距离简称更新状态。最后的“状态”就是SPFA的计算结果)。把s出队,状态有更新的邻居入队,没更新的不入队。也就是说,队列中都是状态有变化的结点,只有这些结点才会影响最短路径的计算。
②现在队列的头部是s的一个邻居u。弹出u,更新其所有邻居的状态,把其中有状态变化的邻居入队列。
③这里有一个问题,弹出u之后,在后面的计算中u可能会再次更新状态(后来发现,u借道其他结点去s,路更近)。所以,u可能需要重新入队列。这一点很容易做到:在处理一个新的结点v时,它的邻居可能就是以前处理过的u,如果u的状态变化了,把u重新加入队列就行了。
④继续以上过程,直到队列空。这也意味着所有结点的状态都不再更新。最后的状态就是到起点s的最短路径。
上面的第(3)点决定了SPFA的效率。有可能只有很少结点重新进入队列,也有可能很多。这取决于图的特征,即使两个图的结点和边的数量一样,但是边的权值不同,它们的SPFA队列也可能差别很大。所以,SPFA是不稳定的。
在比赛时,有的题目可能故意卡SPFA的不稳定性:如果一个题目的规模很大,并且边的权值为非负数,它很可能故意设置了不利于SPFA的测试数据。此时不能冒险用SPFA,而是用Dijkstra算法。Dijkstra算法是一种稳定的算法,一次迭代至少能找到一个结点到s的最短路径,最多只需要m(边数)次迭代即可完成。
链式前向星介绍
(链式前向星的优点)
存储效率高、程序简单、能存储重边
以结点2为例,从点2出发的边有4条,即(2,1)(2,3)(2,4)(2,5),邻居是1、3、4、5 。
①定位第1个边。用head[ ]数组实现,例如head[ 2 ]指向结点2的第1个边,head[ 2 ] = 8 ,它存储在 edge[ 8 ] 这个位置。
②定位其他边。用struct edge 的next参数指向下一个边。edge[ 8 ] .next = 6,指向下一边在edge[ 6 ]这个位置,然后edge[ 6 ].next = 4,edge[ 4 ].next = 1,最后edge[ 1 ].next = -1 , -1表示结束。
struct edge的to参数记录这个边的邻居结点。例如edge[ 8 ].to = 5,第一个邻居是点5;然后edge[ 6 ].to = 4,edge[ 4 ].to = 3,edge[1].to = 1,得到邻居是1、3、4、5。
由于链式前向星用静态数组来模拟邻接表,没有任何浪费,故是空间效率最高的存储方法。
SPFA算法思路详细
具体参考这篇博客:
SPFA算法实例分析 【图解+详细松弛操作】
模板-链式前向星
HDU 2544的SPFA算法代码(链式前向星)
#include<bits/stdc++.h>
#define endl '\n'
#define mst(a,b) memset(a,b,sizeof(a))
using namespace std;
const int maxn=1000005;
const int INF=INT_MAX/10;
struct node{
int to,ne,w;
}stu[maxn];
int n,m,cnt;
int head[maxn]; //头结点静态数组
int dis[maxn]; //记录所有节点到源点的距离
int Neg[maxn]; //判断负圈(Negative loop)
bool vis[maxn]; vis[i]=true表示结点i在队列中
void init(){ //初始化
for(int i=0;i<maxn;i++){
head[i]=-1;
stu[i].ne=-1;
}
cnt=0;
}
void add(int u,int v,int w){ //前向星存图
stu[cnt].to=v;
stu[cnt].w=w;
stu[cnt].ne=head[u];
head[u]=cnt++;
}
int spfa(int s){
memset(Neg,0,sizeof(Neg));
Neg[s]=1;
for(int i=1;i<=n;i++)
dis[i]=INF,vis[i]=false; //初始化
dis[s]=0; //源点到自己的距离为0
queue<int> Q;
Q.push(s);
vis[s]=true; //源点进队列
while(!Q.empty()){
int u=Q.front(); Q.pop();
vis[u]=false;
for(int i=head[u];~i;i=stu[i].ne){ //~i也可以写成i!=-1
int v=stu[i].to;
int w=stu[i].w;
if(dis[v]>dis[u]+w){ //u的第i个邻居v,它借道u,到s更近
dis[v]=dis[u]+w; //更新第i个邻居到s的距离
if(!vis[v]){
vis[v]=true;
Q.push(v);
Neg[v]++;
if(Neg[v]>n) return 1; //出现负圈情况
}
}
}
}
return 0;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
while(cin>>n>>m&&(n+m)){
init();
while(m--){
int u,v,w;
cin>>u>>v>>w;
add(u,v,w);
add(v,u,w);
}
spfa(1);
cout<<dis[n]<<endl;
}
return 0;
}
学如逆水行舟,不进则退