在学完Dijkstra算法后,又学习了可以计算负边的SPFA算法。当即就找了一个简单的求单源最短路经的题来练手:
这个题增添了“每次邮递员送完信后都要返回起点(并且不能走来时的路)”这一条件,就意味着不能用一次单源最短路径进行计算。除了要计算起点到各个点的最短距离,还要计算从各个点回到起点的最短距离。
最初看题想到的是在每个点都进行一次“单源最短路经”计算,但观察数据范围就可以发现,如果对每个点使用SPFA,最坏复杂度达到了O(N^3),是过不了这道题的。另外,起点外的点计算最短路,我们所需的只是从该点到起点的距离,有很多计算都被浪费了。那么,我们就得想其他办法。
我们可以想到,如果有一条从A->B的最短有向路径,如果我们将路径中的所有边都反过来,是不是就可以得到从B->A的最短有向路径了呢。受此启发,既然我们从各个点计算的单源最短路经都是从该点到其他点的最短路径,那如果我们反向建图,是不是就同样可以得到从其他点到该点的最短路径了呢。
因此,我们在正向建图的同时,也进行反向建图,然后再在起点使用一次SPFA,就可以得到从其他点到起点的最短距离了。
另外需要说明的是,从最坏情况看,使用堆优化的Dijkstra复杂度会比SPFA小,不过因为是练习算法,因此该题使用了SPFA来解。
在计算最短路问题中,通常使用Dijkstra,因为它更稳定。但当存在负权路时,Dijkstra不能使用,就需要改用SPFA。另外,SPFA可以判断是否存在负权回路:如果一个阶段入队了n-1次以上,则一定存在负权回路。
以下是AC代码,在代码中有适当的注释,表明了SPFA算法的思想。
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
#include <vector>
using namespace std;
struct path{
int des;
int len;
path(){}
path(int d,int l):des(d),len(l){}
};
vector<path> buf[1005];//邻接表记录图
vector<path> buf1[1005];//反向图
bool visit[1005];//记录节点i是否在候选队列中
int dis[1005];//起点到各个点的最短距离
int dis1[1005];//各个点到起点的最短距离
int n,m;
void SPFA(int x,vector<path> buf[1005],int dis[1005]){
queue<int> q;//候选队列,在候选队列中的节点等待被扩展(松弛)
q.push(x);
visit[x]=true;
dis[x]=0;
vector<path>::iterator it;
while(!q.empty()){
int temp=q.front();
q.pop();
visit[temp]=false;//temp出队后,标记不在队列
for(it=buf[temp].begin();it!=buf[temp].end();it++){
if(dis[temp]+it->len<dis[it->des]){//如果经过当前这条边(u,v)能使到v的距离缩短,则更新距离
dis[it->des]=dis[temp]+it->len;
if(!visit[it->des]){//如果更新后,v不在队列,则入队,用于松弛v能到达的点
q.push(it->des);
visit[it->des]=true;
}
}
}
}
}
int main(){
cin>>n>>m;
int u,v,w;
for(int i=0;i<m;i++){
cin>>u>>v>>w;
buf[u].push_back({v,w});
buf1[v].push_back({u,w});//反向建图
}
memset(dis,127, sizeof(dis));
memset(dis1,127, sizeof(dis1));
SPFA(1,buf,dis);
memset(visit,0, sizeof(visit));
SPFA(1,buf1,dis1);//计算各个点到起点的最短距离
long long int res=0;
for(int i=1;i<=n;i++){
res+=(dis[i]+dis1[i]);
}
cout<<res;
return 0;
}