图论:SPFA 算法详解( 算法竞赛入门到进阶) HDU 2544 链式前向星 【提供ACM模板+图解,不会都难!】

参考书籍:算法竞赛入门到进阶 罗勇军


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;
}
学如逆水行舟,不进则退
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一百个Chocolate

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值