zoukankan      html  css  js  c++  java
  • 最短路径之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
    ",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;
    }
    

      

    加油啦!加油鸭,冲鸭!!!
  • 相关阅读:
    tensorflow1.0 矩阵相乘
    tensorflow1.0 变量加法
    python 给字典按值排序,同样适合于其他
    pytorch 孪生神经网络DNN
    python 利用numpy同时打乱列表的顺序,同时打乱数据和标签的顺序
    python os模块获取指定目录下的文件列表
    创建自定义ssl证书用于https
    使用Maven命令行下载依赖库
    JAVA入门各种API参考
    在centos 6.9 x64下安装code::blocks步骤
  • 原文地址:https://www.cnblogs.com/clarencezzh/p/10382939.html
Copyright © 2011-2022 走看看