zoukankan      html  css  js  c++  java
  • 最短路问题的三种基本算法(模板)

    了解了优先队列,本来想写一道题目练练手,结果就看到了8441,看着像是bfs求最短路,然而T了,并不知道怎么优化,然后又去找老师要了标程,结果神仙代码看不懂(主要是因为太菜..),看到里面用了dijstra,就干脆先从最短路问题入手。

    最短路问题,一般有三种方法,dijstra,bellman-forward,floyed,三者个有特色,适合于不同的场合。

    一。dijstra(迪杰斯特拉)

     

    Dijkstra算法

    1.定义概览

    Dijkstra(迪杰斯特拉)算法是典型的单源最短路径算法,用于计算一个节点到其他所有节点的最短路径。主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。该算法无法处理负权边。

    问题描述:在无向图 G=(V,E) 中,假设每条边 E[i] 的长度为 w[i],找到由顶点 V0 到其余各点的最短路径。(单源最短路径)

    2.算法描述

    1)算法思想:设G=(V,E)是一个带权有向图,把图中顶点集合V分成两组,第一组为已求出最短路径的顶点集合(用S表示,初始时S中只有一个源点,以后每求得一条最短路径 , 就将加入到集合S中,直到全部顶点都加入到S中,算法就结束了),第二组为其余未确定最短路径的顶点集合(用U表示),按最短路径长度的递增次序依次把第二组的顶点加入S中。在加入的过程中,总保持从源点v到S中各顶点的最短路径长度不大于从源点v到U中任何顶点的最短路径长度。此外,每个顶点对应一个距离,S中的顶点的距离就是从v到此顶点的最短路径长度,U中的顶点的距离,是从v到此顶点只包括S中的顶点为中间顶点的当前最短路径长度。

    2)算法步骤:

    a.初始时,S只包含源点,即S={v},v的距离为0。U包含除v外的其他顶点,即:U={其余顶点},若v与U中顶点u有边,则<u,v>正常有权值,若u不是v的出边邻接点,则<u,v>权值为∞。

    b.从U中选取一个距离v最小的顶点k,把k,加入S中(该选定的距离就是v到k的最短路径长度)。

    c.以k为新考虑的中间点,修改U中各顶点的距离;若从源点v到顶点u的距离(经过顶点k)比原来距离(不经过顶点k)短,则修改顶点u的距离值,修改后的距离值的顶点k的距离加上边上的权。

    d.重复步骤b和c直到所有顶点都包含在S中。

    3.执行动画过程如下图

    4.例题:http://icpc.upc.edu.cn/problem.php?id=2716

    算法实现:

    #include <iostream>
    #include <bits/stdc++.h>
    // dijstra n^2 TLE
    using namespace std;
    const int maxn=5e6+10;
    const int inf=1e9+7;
    struct E
    {
        int v,w;
    };
    vector <E> edge[maxn];
    int in[maxn],dis[maxn];//in 数组表示在集合S内,dis表示到个点的最短距离
    int dijstra(int s,int e,int n)//s 出发点 e 终止点 n 点数
    {
        for (int i=0; i<=n; i++)
            dis[i]=inf;
        dis[s]=0,in[s]=1;
        for (int i=0; i<edge[s].size();i++)
        {
            dis[edge[s][i].v]=edge[s][i].w;
            //printf("to%d=%d
    ",edge[s][i].v,dis[edge[s][i].v]);
        }
        //初始化
        for (int i=0; i<=n; i++)
        {
            int mi=inf,k=s;//找到s点最短距离的点
            for (int j=1; j<=n; j++)
            {
                if (!in[j] && dis[j]<mi)
                {
                    mi=dis[j];
                    k=j;
                }
            }
            in[k]=1;//将最短的新点加入集合S
            int num=edge[k].size();//用新点k去扩展新点
            for (int j=0; j<num; j++)
            {
                int v=edge[k][j].v,w=edge[k][j].w;
                if (!in[v])
                {
                    if (dis[k]+w<dis[v]) //relax
                    {
                        dis[v]=dis[k]+w;
                    }
                }
            }
        }
        return dis[e];
    }
    int main()
    {
        int n,m,t;
        scanf("%d%d%d",&n,&m,&t);
        for (int i=1; i<=m; i++)
        {
            int u,v,w;
            scanf("%d%d%d",&u,&v,&w);
            edge[u].push_back({v,w});
            edge[v].push_back({u,w});
        }
        int ans=dijstra(1,t,n);
        printf("%d
    ",ans);
        return 0;
    }

    其实dj算法就是BFS+贪心,它每次选一个点,然后扩散(bfs)到它的邻点之后,再从所有点中,选出离起点最近的点,继续扩散出去。这样总共n-1次之后,图上所有点离起点的距离必然是最小的。时间复杂度为n^2,n>1000一般稳稳地TLE。

    6.优化:

      考虑到每次都是用最近的那一个结点更新,暴力跑需要n的时间,太慢了。

      怎么样能gkd呢?我们自然可以想到优先队列,因为每次要找的点有鲜明的特征,是距离s最近的点。这样优化后,n变成了logn,所以总的复杂度变为nlogn,瞬间快乐。

    代码实现:

    #include <iostream>
    #include <bits/stdc++.h>
    using namespace std;
    const int maxn=5e6+10;
    const int inf=INT_MAX/2;
    /*struct E
    {
        int v,w;
        bool operator< (const E& b) const
        {
            return w > b.w;
        }
    };*/
    struct E
    {
        int v,w;
        friend bool operator< (E x,E y)
        {
            return x.w>y.w;
        }
        //重载<运算符,使得距离小的优先级大
    };
    /*struct cmp
    {
        bool operator() (const E &x,const E &y) const
        {
            return x.w>y.w;
        }
    };*/
    int vis[maxn],dis[maxn];
    vector <E> edge[maxn];
    int dijheap(int s,int e,int n)
    {
        priority_queue <E> Q;
        for (int i=0; i<=n; i++)
            dis[i]=inf;
        Q.push({s,0});
        dis[s]=0;
        while(!Q.empty())
        {
            E cur=Q.top();//保证取出的队首元素就是距离s最近的
            Q.pop();
            int cv=cur.v;
            if (vis[cv]) continue;
            vis[cv]=1;
            int num=edge[cv].size();
            for (int i=0;i<num;i++)//用这个点去扩展relax
            {
                int v=edge[cv][i].v,w=edge[cv][i].w;
                if (!vis[v])
                {
                    if (dis[v]>dis[cv]+w)
                    {
                        dis[v]=dis[cv]+w;
                        Q.push({v,dis[v]});
                    }
                }
            }
        }
        return dis[e];
    }
    
    int main()
    {
        int n,m,t;
        scanf("%d%d%d",&n,&m,&t);
        for (int i=1; i<=m; i++)
        {
            int u,v,w;
            scanf("%d%d%d",&u,&v,&w);
            edge[u].push_back({v,w});
            edge[v].push_back({u,w});
        }
        int ans=dijheap(1,t,n);
        printf("%d
    ",ans);
        return 0;
    }

    7.小结

    Dijstra算法十分优秀,在使用堆(优先队列)优化的情况下,时间复杂度为nlogn,如果题目中不是单源的最短路,那么可以每个点都作为起点跑一下dj算法,n^2logn。

    缺点:

    dj算法是无法处理负权边的!为什么呢,因为dj算法是贪心BFS,而BFS有一个特点,就是短视! 它只能看到与自己相邻的点的情况,但是对于远方,它就一脸蒙蔽了。如果有两种走法,一种是直接走边长5到达,一种是先走10,再走-20到达,显然,我们的dijstra算法会直接走第一种。

    8.扩展

    其实,dijstra算法,还可以输出最短路的路径,只需要用一个pre数组记录一下每个节点的前驱结点,递归输出就可以了。

    代码实现:

    #include <bits/stdc++.h>
    
    using namespace std;
    typedef long long ll;
    const int maxn=1e3+20;
    const int inf=1e9+7;
    int dis[maxn],vis[maxn],pre[maxn];
    struct E
    {
        int v,w;
        bool friend operator< (E x,E y)
        {
            return x.w>y.w;
        }
    };
    vector <E> edge[maxn];
    void dij(int s,int n)
    {
        priority_queue <E> Q;
        while(Q.size()) Q.pop();
        for (int i=1; i<=n; i++)
            dis[i]=inf;
        dis[s]=0;pre[s]=s;
        Q.push({s,0});
        while(!Q.empty())
        {
            E cur=Q.top();
            Q.pop();
            int cv=cur.v;
            int num=edge[cv].size();
            if (vis[cv]) continue;
            vis[cv]=1;
            for (int i=0; i<num; i++)
            {
                int v=edge[cv][i].v,w=edge[cv][i].w;
                if (dis[v]>dis[cv]+w)
                {
                    dis[v]=dis[cv]+w;
                    Q.push({v,dis[v]});
                    pre[v]=cv;
                }
            }
        }
    }
    void outway(int i)
    {
        if (pre[i]!=i)
        {
            printf("%d-->",i);
            outway(pre[i]);
        }
        else printf("1
    ");
        return ;
    }
    int main()
    {
        int n,m;
        freopen("out2.txt","w",stdout);
        while(~scanf("%d%d",&n,&m))
        {
            if (n==0&&m==0) break;
            memset(vis,0,sizeof(vis));
            memset(edge,0,sizeof(edge));
            memset(pre,0,sizeof(pre));
            for (int i=1; i<=m; i++)
            {
                int u,v,w,flag=0;
                scanf("%d%d%d",&u,&v,&w);
                edge[u].push_back({v,w});
            }
            dij(1,n);
            for (int i=2; i<=n; i++)
                i==n ? printf("%d
    ",dis[i]) :printf("%d ",dis[i]);
            for (int i=2; i<=n; i++)
                outway(i);
        }
        return 0;
    }

    需要正序输出的话,其实也可以,用stack记录一下路径即可

    代码如下:

    void callway(int i,int x)
    {
        while(pre[i]!=i)
        {
            way[x].push(i);
            i=pre[i];
        }
        way[x].push(1);
    }
    void outway(int x)
    {
       while(way[x].size())
       {
           int a=way[x].top();
           way[x].pop();
           if (way[x].size()==0)
                printf("%d
    ",a);
           else
            printf("%d-->",a);
       }
    }
    
    for (int i=2; i<=n; i++)
                callway(i,i),outway(i);

     

    二。Bellman-Ford 算法

    1.定义概览

    Bellman - ford算法是求含负权图的单源最短路径的一种算法,效率较低(nm),代码难度较小。其原理为连续进行松弛,在每次松弛时把每条边都更新一下,若在n-1次松弛后还能更新,则说明图中有负环,因此无法得出结果,否则就完成。

    问题描述:在无向图 G=(V,E) 中,假设每条边 E[i] 的长度为 w[i],找到由顶点 V0 到其余各点的最短路径,边长可能为负值。(单源最短路径)

    2.算法描述

    每一条边松弛n次,对于任意一条最短路,最多松弛n-1次,如果还能松弛,说明存在负环

    3.算法步骤

    a.初始化

    b.对每个点所连的边松弛

    3.松弛检查负环

    4.例题 http://icpc.upc.edu.cn/problem.php?id=1634

    代码如下:

    #include <iostream>
    #include <bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const int maxn=2e4+10;
    const ll inf=2147483647;
    struct E
    {
        int v,w;
    };
    vector<E> edge[maxn];
    ll dis[maxn];
    bool bellman(int s,int n)
    {
        for (int i=1; i<=n; i++)
            dis[i]=inf;
        dis[s]=0;
        for (int k=1; k<=n;k++)
        for (int i=1; i<=n; i++)
        {
            for (int j=0; j<edge[i].size();j++)
            {
                ll v=edge[i][j].v,w=edge[i][j].w;
                if (dis[v]>dis[i]+w)
                    dis[v]=dis[i]+w;
            }
        }
        for (int i=1; i<=n; i++)
        {
            for (int j=0; j<edge[i].size();j++)
            {
                int v=edge[i][j].v,w=edge[i][j].w;
                if (dis[v]>dis[i]+w)
                    return 1;
            }
        }
        return 0;
    }
    int main()
    {
        int n,m,s;
        scanf("%d%d%d",&n,&m,&s);
        for (int i=1; i<=m; i++)
        {
            int u,v,w;
            scanf("%d%d%d",&u,&v,&w);
            edge[u].push_back({v,w});
        }
        int flag=bellman(s,n);
        for (int i=1; i<=n-1; i++)
        {
            printf("%lld ",dis[i]);
        }
        printf("%lld
    ",dis[n]);
       flag ? cout<<"Yes " : cout<<"No ";
    return 0; }

    5.优化(SPFA)

    朴素的bellman-ford算法时间复杂度是n*m,很容易超时

    很多时候我们并不需要那么多次松弛,只有上一次被松弛的结点,所连接的边,才有可能引起下一次的松弛。

    那么我们就用队列维护<那些结点可能会引起松弛>,就可以至访问必要的边了。

    优化以后时间复杂度会是k*m,k为常数且很小。

    代码如下:

    #include <iostream>
    #include <bits/stdc++.h>
     
    using namespace std;
    struct E
    {
        int v,w;
    };
    const int maxn=2e4+10,inf=1e9+7;
    vector <E> edge[maxn];
    int in[maxn],dis[maxn];
    void SPFA(int s,int n)
    {
        for (int i=1;i<=n;i++) dis[i]=inf;
        dis[s]=0;
        queue <int> Q;
        Q.push(s);
        in[s]=1;
        while(!Q.empty())
        {
            int cur=Q.front();
            Q.pop();
            in[cur]=0;
            int num=edge[cur].size();
            for (int i=0; i<num; i++)
            {
                int v=edge[cur][i].v,w=edge[cur][i].w;
                if (dis[v]>dis[cur]+w)
                {
                    dis[v]=dis[cur]+w;
                    if (!in[v])
                        Q.push(v),in[v]=1;
                }
            }
        }
    }
    int main()
    {
        int n,m;
        scanf("%d%d",&n,&m);
        for (int i=1; i<=m; i++)
        {
            int u,v,w;
            scanf("%d%d%d",&u,&v,&w);
            edge[u].push_back({v,w});
        }
        SPFA(1,n);
        for (int i=2;i<=n;i++)
        {
            printf("%d
    ",dis[i]);
        }
        return 0;
    }

    6.小结

    bellman-ford算法比较好写,但是时间复杂度不够好,即使是优化过的SPFA算法,也很容易被精心设计的稠密图给卡掉,毕竟理论上界还是n*m。

    但是对于负权边或者判断负环,我们就必须使用bellman*ford算法了。

    7.扩展

    关于玄学复杂度的玄学优化

     SLF优化

    SLF叫做Small Label First 策略。

    比较当前点和队首元素,如果小于队首,则插入队首,否则加入队尾。

    具体为啥可以优化,其实也是玄学,甚至对于有的数据,优化还会变慢。。。其实SPFA被卡的话,我觉得优化也没有意义的,所以只能算是锦上添花吧,不是很必要掌握(万一哪天水过了呢

    代码如下:

    #include <iostream>
    #include <bits/stdc++.h>
    using namespace std;
    const int maxn=5000010;
    const int inf=1e9+7;
    struct E
    {
        int v,w;
    };
    vector <E> edge[maxn];
    int in[maxn],dis[maxn];
    int SPFA(int s,int e,int n)
    {
        for (int i=1; i<=n; i++)
            dis[i]=inf;
        dis[s]=0;
        deque <int> Q;
        Q.push_back(s);
        in[s]=1;
        while(!Q.empty())
        {
            int cur=Q.front();
            Q.pop_front();
            in[cur]=0;
            int num=edge[cur].size();
            for (int i=0; i<num;i++)
            {
                int v=edge[cur][i].v,w=edge[cur][i].w;
                if (dis[v]>dis[cur]+w)
                {
                    dis[v]=dis[cur]+w;
                    if (!in[v])
                    {
                        if (dis[v]<=dis[cur]) Q.push_front(v);
                        else Q.push_back(v);
                        in[v]=1;
                    }
                }
            }
        }
        return dis[e];
    }
    int main()
    {
        int n,m,t;
        scanf("%d%d%d",&n,&m,&t);
        for (int i=1; i<=m; i++)
        {
            int u,v,w;
            scanf("%d%d%d",&u,&v,&w);
            edge[u].push_back({v,w});
            edge[v].push_back({u,w});
        }
        int ans=SPFA(1,t,n);
        printf("%d
    ",ans);
        return 0;
    }

    三。Floyed算法(弗洛伊德算法)

    1.定义概览

    一种可以求出任意两点之间最短路的算法,支持正负权,可以实现传递闭包,时间复杂度N^3

    2.算法描述

    1)算法思想原理:

         Floyd算法是一个经典的动态规划算法。用通俗的语言来描述的话,首先我们的目标是寻找从点i到点j的最短路径。从动态规划的角度看问题,我们需要为这个目标重新做一个诠释(这个诠释正是动态规划最富创造力的精华所在)

          从任意节点i到任意节点j的最短路径不外乎2种可能,1是直接从i到j,2是从i经过若干个节点k到j。所以,我们假设Dis(i,j)为节点u到节点v的最短路径的距离,对于每一个节点k,我们检查Dis(i,k) + Dis(k,j) < Dis(i,j)是否成立,如果成立,证明从i到k再到j的路径比i直接到j的路径短,我们便设置Dis(i,j) = Dis(i,k) + Dis(k,j),这样一来,当我们遍历完所有节点k,Dis(i,j)中记录的便是i到j的最短路径的距离。

    2).算法描述:

    a.从任意一条单边路径开始。所有两点之间的距离是边的权,如果两点之间没有边相连,则权为无穷大。   

    b.对于每一对顶点 u 和 v,看看是否存在一个顶点 w 使得从 u 到 w 再到 v 比己知的路径更短。如果是更新它。

    3.代码实现

    memset(dis,0x3f,sizeof(dis));
    for (int i=1; i<=n; i++)
        dis[i][i]=0;
    for (int k=1; k<=n; k++)
        for (int i=1; i<=n; i++)
            for (int j=1; j<=n; j++)
                dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);

    4.扩展

    对于有向图,有时我们只关心两点之间是否有通路,可以用0/1表示。然后吧循环语句改成 a[i][j]|=(a[i][k]&&a[k][j]);就可以啦

    例题和代码:http://acm.zju.edu.cn/onlinejudge/showProblem.do?problemCode=4124

    #include <iostream>
    #include <bits/stdc++.h>
    using namespace std;
    const int  maxn=105;
    int a[maxn][maxn],ma[maxn],mi[maxn];
    int main()
    {
        int T;
        cin>>T;
        while(T--)
        {
            memset(a,0,sizeof(a));
            memset(ma,0,sizeof(ma));
            memset(mi,0,sizeof(mi));
            int n,m,flag=0;
            scanf("%d %d",&n,&m);
            for (int i=1; i<=m; i++)
            {
                int u,v;
                scanf("%d %d",&u,&v);
                if (u==v)  flag=1;
                a[u][v]=1;
            }
            for (int k=1; k<=n; k++)
            {
                for (int i=1; i<=n; i++)
                {
                    for (int j=1; j<=n; j++)
                    {
                        a[i][j]=a[i][j]||(a[i][k]&&a[k][j]);
                    }
                }
            }
            for (int i=1; i<=n; i++)
            {
                for (int j=1; j<=n; j++)
                {
                    if (a[i][j]&&a[j][i])
                    {
                        flag=1;
                        break;
                    }
                }
                if (flag) break;
            }
            if (flag)
            {
                for (int i=1; i<=n; i++)
                    putchar(48);
                putchar(10);
                continue ;
            }
            for (int i=1; i<=n;i++)
            {
                for (int j=1; j<=n; j++)
                {
                    if (a[i][j])
                        ma[i]++,mi[j]++;
                }
            }
            for (int i=1; i<=n; i++)
            {
                if (ma[i]<=n/2 && mi[i]<=n/2)
                    putchar(49);
                else putchar(48);
            }
            putchar(10);
        }
        return 0;
    }

    四。总结

    最短路算法是图论里最基础的算法,根据不同的情况,我们要有合适的选择。

    对于负权边和判断负环,一般用SPFA;

    对于需要输出路径的,一般用dijstra;

    对于判断联通的,一般用floyd;

    当题目没有特别强调有负权边时,一般应该选择dijstra,因为spfa很容易被人卡时间。除非数据很水。

  • 相关阅读:
    查询对象模式(下)
    淘宝code
    C#中使用消息队列RabbitMQ
    MVC5模板部署到mono
    ventBroker简单实现
    http协议知识整理(转)
    创业者应该有的5个正常心态(转)
    观点:独立游戏开发者创业路上的11个‘坑’(转)
    应用程序框架实战三十四:数据传输对象(DTO)介绍及各类型实体比较(转)
    【技巧篇】解决悬浮的<header>、<footer>遮挡内容的处理技巧(转)
  • 原文地址:https://www.cnblogs.com/ztdf123/p/10891720.html
Copyright © 2011-2022 走看看