最短路——SPFA算法

简介:

解决单源最短路径问题
用队列优化的Bellman-Ford算法
把源点的最短路径估计值设为0,入队
一般不使用,使用Dijstra,不过在某些问题可能会有特殊用途
O(kE) k是一个常数,一般为2左右,但是极端情况下会出现O(VE),所以其复杂度不是稳定的

步骤思想:

1:起点s入队,计算它所有邻居到s的最短距离(当前的最短距离(SPFA的本质就是通过枚举点的边的加入是否会对当前点的最短路径产生影响来判定的),不是全局最短距离。在此,把计算一个结点到起点s的最短路径称为更新状态。最后的状态就是SPFA的结果)。把s出队,状态有更新的邻居入队,没更新的就不用入队。(队列中只有状态产生变化的点,因为只有这些点才会对最短路径的计算产生影响)

2:现在队列中的头是u(s的一个邻居)。弹出u,更新其所有邻居的状态,把其中状态有变化的邻居入队列。(这里需要注意的是,弹出u之后,在后面的计算中u可能会再次更新状态(后来发现,u借道其他结点去s,路会更近),所以u会再次入队)

3:继续以上过程,直到队列为空为止或者某一的点入队超过了n(结点数)次(这是因为存在了负圈)

 

适用于判断存在负圈:

1:如果一个点进队列超过n次,就存在负圈。可以通过一个数组Neg[]来记录一个点入队列的次数
2:记录从S到当前点路径上经过了多少个点,超过N则存在负圈
3:开一个数组vis,记录某个点是否入栈。沿着当前所连边一直dfs下去,知道遇到某个点当前已经入栈,就找到了负环。因为一个点在最短路上出现多次,就一定有负环

 

写于2020/8/10 22:28

 

例题:https://www.luogu.com.cn/problem/P1186

题解:这道题比较简单就是先求出一条最短路来,然后遍历的删除边上的每一条边,再求最短路

AC代码:

#include<iostream>
#include<queue>
#include<cstring>
using namespace std;
const int N=1005;
int n,m,dis[N],pre[N],cnt=0;//dis表示结点i到源点的最短距离;pre表示结点i最短路径上的前一条边,而不是前一个点 
int vis[N]; //标记节点i是否需要入队 
int head[N];//链式前向星 
struct st{
    int from,to,next,dis;
}edge[N*N];
bool flag=false;
void addedge(int u,int v,int w){//链式前向星添加边。其实就是一个数组模拟链表,并且新加的边都放在了结点头之后 
    cnt++;
    edge[cnt].from=u;
    edge[cnt].to=v;
    edge[cnt].dis=w;
    edge[cnt].next=head[u];
    head[u]=cnt;
}
void spfa(int s){
    memset(dis,0x3f,sizeof(dis));//0x3f3f3f3f是int的无穷大 
    memset(vis,0,sizeof(vis));
    dis[s]=0;
    queue<int>q;
    q.push(s);
    vis[s]=1;//表示s已经在队列中了 
    while(!q.empty()){
        int now=q.front();q.pop();
        vis[now]=0;
        for(int i=head[now];i;i=edge[i].next){
            if(dis[edge[i].to]>dis[edge[i].from]+edge[i].dis) {
                dis[edge[i].to]=dis[edge[i].from]+edge[i].dis;
                if(!flag) pre[edge[i].to]=i;//只有当flag=false,才会更新pre 
                if(!vis[edge[i].to]){//只有这个点没有在队列中时,才会入队 
                    q.push(edge[i].to);
                    vis[edge[i].to]=1;
                }
            }
        }
    }
}
int main(){
    cin>>n>>m;
    int u,v,w;
    for(int i=1;i<=m;i++){
        cin>>u>>v>>w;
        addedge(u,v,w);
        addedge(v,u,w);
    }
    spfa(1);
    flag=true;
    int ans=dis[n];
    for(int i=pre[n];i!=0;i=pre[edge[i].from]){//i是边的编号,而不是点的编号,因为在链式前向星中,知道两个点,但是求这两个点之间的边并不好求 
        int old=edge[i].dis;
        edge[i].dis=1e7; 
        //edge[i].dis=0x3f3f3f3f;//int的无穷大 
        spfa(1);
        ans=max(ans,dis[n]);
        edge[i].dis=old;
    }
    cout<<ans;
    return 0;
}

修改于 2020/8/11 8:17

 

注:在spfa中使用的是普通的队列,而不是优先队列,不要跟dijkstra搞混。spfa队列中存储的是结点的编号,并不需要其余多余的信息,不像dijkstra还需要存储当前权值。spfa拿出队列头部元素,需要更新与其直接相连结点(有向边)的权值,并且同时更新相连结点的dis,只有当dis改变,并且这个结点没有在队列中,才需要将这个结点放入队列中去。

修改于:2020/8/14 22:57

 

spfa可以用于求解最短路,即使图中存在正环也无所谓。需要注意的是spfa也可以用于求解最长路,但是这要保证图中不能存在正环才可以,当然在这里是可以存在负环的。同理在求解最短路的时候是不能存在负环,但是却可以存在正环。

补于:2021/1/22 15:07

 

posted @ 2020-08-10 22:29  白菜茄子  阅读(247)  评论(0编辑  收藏  举报