最短路径之spfa

引入1:单源最短路

问:求带权有向图上一个源点到其他点的最短路径距离

如果没有非负边权,我们自然可以想到dij。但是如果有负边权呢?这时候就要用SPFA算法求解。

原理&讲解

用dis数组记录源点到有向图上任意一点距离,其中源点到自身距离为0,到其他点距离为INF。将源点入队,并重复以下步骤:

  • 队首x出队
  • 遍历所有以队首为起点的有向边(x,i),若dis[x]+w(x,i)<dis[i],则更新dis[i]
  • 如果点i不在队列中,则i入队
  • 若队列为空,跳出循环;否则执行1

实际上我们可以将其理解为bfs

如果图是随机生成的,时间复杂度为 O(KM) (K可以认为是个常数,m为边数,n为点数)

但是实际上SPFA的算法复杂度是 O(NM) ,可以构造出卡SPFA的数据,让SPFA超时。

在NOI 2018的第一天第一题中,出题人卡了SPFA算法,导致100分变成60分,所以在没有负环、单纯求最短路径,不建议使用SPFA算法,而是用Dijkstra算法。

在初一我们学到一条三角形中的性质,即同一三角形内两边之和大于第三边。而最短路中如u->v的最短路它是小于等于其它任意路径的,这使我们容易yy到三角形。也就是说,我们实际上每次都是在判断这条路径符不符合三角形不等式,若不符合,我们就将原先的路径松弛为现在的路径,使得现在的路径满足三角形不等式。但是为什么松弛后要将终点入队呢?SPFA的过程是BFS,它是不停扩展节点的。而当我们更新了这一条路径,那么可能会出现基于这一条路径的新路,我们需要判断原路与新路是否满足三角形不等式。

模拟&代码

我们可以手推这张图模拟一下~

我们以1为源点,初始化:dis[源点]=0,其他为正无穷,并将源点入队。

队首1出队,并枚举它的出边1->2,1->3。由dis[1]+w(1,2)=1<dis[2]=INF,dis[1]+w(1,3)=6<dis[3]=INF得dis[2]=dis[1]+w(1,2)=1,dis[3]=dis[1]+w(1,3)=6,并将2,3入队。

队首2出队,枚举它的出边2->3,2->4,2->5。都不满足三角形不等式,所以松弛它们。并将3,4,5入队,但由于3已在队内,所以不管。

队首3出队,没有能松弛的边,直接略过。

此时队内剩下4,5,由于这两点没有出边,所以在此不枚举。

手绘勿喷

下面是带注释代码:

#include<iostream>
#include<vector>
#include<algorithm>
#include<cstring>
#include<string>
#include<cstdio>
#include<cstdlib>
#include<queue>
#define N 110000
#define INF 0x3f3f3f3f
using namespace std;

int n,m,a,b,c,vis[N],dis[N];

struct node
{
    int d,w;
};//定义一个结构体来存储每个入度点以及对应边的权值
//比如边u->v,权值为w,node结构体存储的就是v以及w。

vector<node>v[N];

void spfa(int u);

int main()
{
    //对于N非常大但是M很小的这种稀疏图来说,用邻接矩阵N*N是存不下的。邻接矩阵是将所有的点都存储下来了,然而
    //对于稀疏图来说,有很多点是没有用到的,把这些点也存储下来的话就会很浪费空间。可以用邻接表来存储,这里借助vector来实现邻接表的操作。
    //用邻接表存储时候,只存储有用的点,对于没有用的点不存储,实现空间的优化。
    cin>>n>>m;

    for(int i=0; i<=n; i++)
        v[i].clear();//将vecort数组清空
    for(int i=1; i<=m; i++) //用vector存储邻接表
    {
        node nd;
        scanf("%d%d%d",&a,&b,&c);
        nd.d=b,nd.w=c;//将入度的点和权值赋值给结构体
        v[a].push_back(nd);//将每一个从a出发能直接到达的点都压到下标为a的vector数组中,以后遍历从a能到达的点就可以直接遍历v[a]
        //        nd.d=a,nd.w=c;//无向图的双向存边
        //        v[b].push_back(nd);
    }
    spfa(1);
    if(dis[n]!=INF)
        printf("%d\n",dis[n]);
    else
        printf("impossible");
    return 0;
}
void spfa(int u){
    memset(vis,1,sizeof(vis));
    memset(dis,0x3f,sizeof(dis));
    dis[u]=0;
    queue<int> q;
    q.push(u);
    vis[u]=false;
    while (!q.empty()) {
        int x=q.front();
        q.pop();
        vis[x]=true;
        vector<node> s=v[x];
        for (int i = 0; i < s.size(); ++i) {
            int v=s[i].d;
            if(dis[x]+s[i].w<dis[v]){
                dis[v]=dis[x]+s[i].w;
                if(vis[v]){
                    q.push(v);
                    vis[v]=false;
                }
            }
        }
    }
}

引入2:判正(负)环

spfa算法还可以在有向图内判正环负环,我们可以使用DFS/BFS版SPFA。注意,判负环跑最短路,判正环跑最长路。

#include<iostream>
#include<vector>
#include<algorithm>
#include<cstring>
#include<string>
#include<cstdio>
#include<cstdlib>
#include<queue>
#define N 110000
#define INF 0x3f3f3f3f
using namespace std;

int n,m,a,b,c,instack[N],dis[N],flag;

struct node
{
    int d,w;
};//定义一个结构体来存储每个入度点以及对应边的权值
//比如边u->v,权值为w,node结构体存储的就是v以及w。

vector<node>v[N];

void spfa(int u);

int main()
{
    //对于N非常大但是M很小的这种稀疏图来说,用邻接矩阵N*N是存不下的。邻接矩阵是将所有的点都存储下来了,然而
    //对于稀疏图来说,有很多点是没有用到的,把这些点也存储下来的话就会很浪费空间。可以用邻接表来存储,这里借助vector来实现邻接表的操作。
    //用邻接表存储时候,只存储有用的点,对于没有用的点不存储,实现空间的优化。
    cin>>n>>m;

    for(int i=0; i<=n; i++)
        v[i].clear();//将vecort数组清空
    for(int i=1; i<=m; i++) //用vector存储邻接表
    {
        node nd;
        scanf("%d%d%d",&a,&b,&c);
        nd.d=b,nd.w=c;//将入度的点和权值赋值给结构体
        v[a].push_back(nd);//将每一个从a出发能直接到达的点都压到下标为a的vector数组中,以后遍历从a能到达的点就可以直接遍历v[a]
        //        nd.d=a,nd.w=c;//无向图的双向存边
        //        v[b].push_back(nd);
    }
    memset(instack,0,sizeof(instack));
    memset(dis,0,sizeof(dis));
    flag=0;
    for(int i=1;i<=n;i++){spfa(i);if(flag)break;}
    if(flag)printf("Yes");
    else printf("No");
    return 0;
}
void spfa(int u){
    if(instack[u]){
        flag=1;
        return;
    }
    instack[u]=true;
    vector<node> s=v[u];
    for (int i = 0; i < s.size(); ++i) {
        if(dis[u]+s[i].w<dis[s[i].d]){
            dis[s[i].d]=dis[u]+s[i].w;
            spfa(s[i].d);
            if(flag)return;
        }
    }
    instack[u]=false;
}

  

 

转载于:https://www.cnblogs.com/clarencezzh/p/10382939.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值