• 浅谈图论(一)——最短路问题


    图论〔Graph Theory〕是数学的一个分支。它以图为研究对象。图论中的图是由若干给定的点及连接两点的线所构成的图形,这种图形通常用来描述某些事物之间的某种特定关系,用点代表事物,用连接两点的线表示相应两个事物间具有这种关系。(摘自百度百科)

    1.Floyd 弗洛伊德算法

    这种算法解决的是多源最短路问题(求任意两点之间的最短路径)

    若我们用二维数组e[i][j]表示点i到点j当前的最短距离(程序运行完后数组中存的就是真正的最短距离)

    那么我们可以用e[i][j]=max(e[i][j],e[i][k],e[j][k]);来更新e数组。也就是比较 从i到j 和 从i到k+从k到j 的距离

    重点来啦!!!

    核心思想:能否找到第三点使得任意两点的距离变短,即能否找到 i->k->j 比 i->j 短,如果能找到,就更新。

    下面呈上代码:

    //多元最短路 Floyd O(n^3)
    #include<iostream>
    #include<cstdlib>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<cmath>
    using namespace std;
    const int maxn=99999999;
    int n,m,e[1005][1005];
    int main()
    {
        int i,j,k,a,b,c;
        scanf("%d%d",&n,&m);
        for(i=1;i<=n;i++)
        {
            for(j=1;j<=n;j++)
            {
                if(i==j) e[i][j];
                else e[i][j]=maxn;
            }
        }
        for(i=1;i<=m;i++)
        {
            scanf("%d%d%d",&a,&b,&c);
            e[a][b]=c;
        }
        //Floyd核心部分 
        for(i=1;i<=n;i++)
            for(j=1;j<=n;j++)
                for(k=1;k<=n;k++)
                    if(e[j][k]>e[j][i]+e[i][k])
                        e[j][k]=e[j][i]+e[i][k];
        for(i=1;i<=n;i++)
        {
            for(j=1;j<=n;j++)
                printf("%d ",e[i][j]);
            printf("
    ");
        }
        return 0;
    }
    Floyd

    很容易发现Floyd算法的时间复杂度是O(N^3)。这个复杂度很高,时间限制为1000ms的情况下,n=1000左右就不行了。
    但它的优点是好写,好理解,可以解决负边权。

    总结一下Floyd:

    时间复杂度:O(N^3)

    优点:好写,好理解,可以解决负边权

    缺点:太慢

    2.Dijkstra 迪杰斯特拉

    这种算法解决的是单源最短路问题(求任意一点与其它点之间的最短路径)

    这种算法也要用e数组,作用同上

    本算法还要用dis数组,我们用dis[i]表示点i到源点当前的最短距离,这个值是不断变化的

    个人感觉Dijkstra比Floyd要难一点

    方法:

    1.我们可以把所有的点分为两个集合,集合p和集合q,用数组book表示。book[i]==1的为p表示选过的点,book[j]==0为q表示没选过的点.;

    2.从q集合中选择一个离源点最近的点,即 使dis[u]最小,将该点u加入p集合。并搜索每一条以u为起点的边进行松弛,即比较dis[v]和dis[u]+e[u][v];

    3.重复第2步,直到q集合为空。此时dis数组中存的就是最终结果。

    下面呈上代码:(默认源点为1号顶点)

    #include<iostream>
    #include<cstdlib>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<cmath>
    using namespace std;
    const int maxn=99999999;
    int n,m,e[1005][1005],dis[1005],book[1005]; 
    int main()
    {
        int i,j,a,b,c;
        scanf("%d%d",&n,&m);
        for(i=1;i<=n;i++)
        {
            for(j=1;j<=n;j++)
            {
                if(i==j) e[i][j];
                else e[i][j]=maxn;
            }
        }
        book[1]=1;
        for(i=1;i<=m;i++)
        {
            scanf("%d%d%d",&a,&b,&c);
            e[a][b]=c;
        }
        for(i=1;i<=n;i++) dis[i]=e[1][i];
        for(i=1;i<=n-1;i++)
        {
            int minn=maxn,u;
            for(j=1;j<=n;j++)
            {
                if(dis[j]<minn && book[j]==0)
                {
                    minn=dis[j];
                    u=j;
                }
            }
            book[u]=1;
            for(j=1;j<=n;j++)
            {
                if(e[u][j]<maxn)
                {
                    if(dis[j]>dis[u]+e[u][j])
                        dis[j]=dis[u]+e[u][j];
                }
                    
            }
        }
        for(i=1;i<=n;i++) printf("%d ",dis[i]);
        return 0;
    }
    Dijkstra

     Dijkstra的时间和空间复杂度都是O(n^2)

    此外,我们发现Dijkstra在找到u点后需要把图扫一遍,所以可以用邻接表来优化

    下面呈上Dijkstra邻接表优化的代码:

    #include<iostream>
    #include<cstdlib>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<cmath>
    using namespace std;
    const int maxn=99999999;
    int n,m,dis[1005],book[1005];
    int b[1005],c[1005];
    int first[1005],next[1005]; 
    int main()
    {
        int i,j,x,y,z;
        scanf("%d%d",&n,&m);
        //book[1]=1;
        for(i=1;i<=m;i++)//建立邻接表 
        {
            scanf("%d%d%d",&x,&y,&z);
            b[i]=y;
            c[i]=z;
            next[i]=first[x];//建表的关键 
            first[x]=i;
        }
        memset(dis,88,sizeof(dis));
        dis[1]=0;
        //Dijkstra的核心部分 
        for(i=1;i<=n-1;i++)
        {
            int minn=maxn,u;
            for(j=1;j<=n;j++)
            {
                if(dis[j]<minn && book[j]==0)
                {
                    minn=dis[j];
                    u=j;
                }
            }
            book[u]=1;
            for(j=first[u];j;j=next[j])
            {
                dis[b[j]]=min(dis[b[j]],dis[u]+c[j]);
            }
        }
        for(i=1;i<=n;i++) printf("%d ",dis[i]);
        return 0;
    }
    Dijkstra邻接表优化

    优化后,时间变成了O((M+N) log N),空间降到了O(M)。一般的,m会比n^2小很多,所以优化后总体较优。

    总结一下Dijkstra:

    时间复杂度:O((M+N) log N)

    优点:时间空间复杂度都较低,思路值得借鉴

    缺点:不能解决负边权

    3.Bellman-Ford 贝尔曼福德算法

    个人最喜欢的最短路算法(同感的点个赞呗)

    这种算法解决的也是单源最短路问题。

    这种算法也需要dis数组,作用同上。

    方法:

    1.枚举每一条边,比如说u->v的边,看看能否通过该边使得dis[v]变短,即比较dis[v]和dis[u]+c[i](c为路径长度)

    2.重复步骤1,每重复一遍称为一遍松弛

    那么需要松弛多少次呢?

    n-1次。因为任意两点之间的路径最多经过n-1条边。

    敲黑板划重点:

    最短路径中不可能包含回路!

    讲完了,呈上代码:

    #include<iostream>
    #include<cstdlib>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<cmath>
    using namespace std;
    int n,m,a[10005],b[10005],c[10005],dis[10005];
    int main()
    {
        int i,j;
        memset(dis,88,sizeof(dis));
        dis[1]=0;
        scanf("%d%d",&n,&m);
        for(i=1;i<=m;i++) scanf("%d%d%d",&a[i],&b[i],&c[i]);
        for(i=1;i<=n-1;i++)
        {
            for(j=1;j<=m;j++)
            {
               if(dis[b[j]]>dis[a[j]]+c[j])
               {
                   dis[b[j]]=dis[a[j]]+c[j];
               }
            }
        }
        for(i=1;i<=n;i++) printf("%d ",dis[i]);
        return 0;
    }
    
    Bellman-Ford
    Bellman-Ford


    此外,Bellman-Ford算法还能判断一个图有没有负权回路,如果松弛完了之后仍存在

    dis[b[j]]>dis[a[j]]+c[j]

    则此图有负权回路,因为从负权回路走一圈可以使最短路径再次变短。

    有人可能会问了:真的必须要松弛n-1次吗?

    答案是:不需要!n-1次只是一个最大值罢了。如果当前的一轮松弛没有作用,那么后面的都不会起作用,此时可以提前跳出循环

    我们将上面几行的描述加入到代码中,如下:

    #include<iostream>
    #include<cstdlib>
    #include<cstdio>
    #include<cstring>
    #include<algorithm>
    #include<cmath>
    using namespace std;
    int n,m,a[10005],b[10005],c[10005],dis[10005];
    int main()
    {
        int i,j,check;
        memset(dis,88,sizeof(dis));
        dis[1]=0;
        scanf("%d%d",&n,&m);
        for(i=1;i<=m;i++) scanf("%d%d%d",&a[i],&b[i],&c[i]);
        for(i=1;i<=n-1;i++)
        {
            check=0;
            for(j=1;j<=m;j++)
            {
               if(dis[b[j]]>dis[a[j]]+c[j])
               {
                   dis[b[j]]=dis[a[j]]+c[j];
                   check=1;
               }
            }
            if(check==0) break;
        }
        for(j=1;j<=m;j++)
               if(dis[b[j]]>dis[a[j]]+c[j])
               {
                      printf("此图有负权回路");
                      return 0;
                  }
        for(i=1;i<=n;i++) printf("%d ",dis[i]);
        return 0;
    }
    Bellman-Ford优化版

     此外,Bellman-Ford算法还有一种队列优化方法:每次仅对最短路估计值发生变化了的顶点的所有出边执行松弛操作。

    我们可以用一个队列来维护这些点,用邻接表来储存图

    下面呈上代码:

    #include<iostream>
    #include<cstdio>
    #include<cstdlib>
    #include<cstring>
    #include<algorithm>
    #include<cmath>
    #include<queue>
    using namespace std;
    int n,m,a[1005],b[1005],c[1005],dis[1005],book[1005],first[1005],next[1005];
    int main()
    {
        int i,x,y,z;
        queue <int> Q;
        scanf("%d%d",&n,&m);
        for(i=1;i<=m;i++)
        {
            scanf("%d%d%d",&x,&y,&z);
            a[i]=x;
            b[i]=y;
            c[i]=z;
            next[i]=first[x];
            first[x]=i;
        }
        memset(dis,88,sizeof(dis));
        dis[1]=0;
        Q.push(1);
        book[1]=1;
        while(!Q.empty())
        {
            for(i=first[Q.front()];i;i=next[i])
            {
                if(dis[b[i]]>dis[a[i]]+c[i])
                {
                    dis[b[i]]=dis[a[i]]+c[i];
                    if(book[b[i]]==0)
                    {
                        Q.push(b[i]);
                        book[b[i]]=1;
                    }
                }
            }
            book[Q.front()]=0;
            Q.pop();
        }
        for(i=1;i<=n;i++) printf("%d ",dis[i]);
        return 0;
    }
    
    Bellman-Ford队列优化
    SPFA

    总结一下Bellman-Ford

    时间复杂度:最坏是O(MN)

    优点:可以解决负边权,空间占用小

    缺点:再快一点就完美了

    4.总结

    Floyd

    Dijkstra

    Bellman-Ford

    空间复杂度

    O(N^2)

    O(M)

    O(M)

    时间复杂度

    O(N^3)

    O((M+N) log N)

    最坏是O(MN)

    单源or多源最短路

    多源

    单源

    单源

    其它

    可以解决负边权

    不能解决负边权

    可以解决负边权

    每种算法都各有所长,我们要根据实际情况,选择最合适的算法。

    ~祝大家编程顺利~

  • 相关阅读:
    私有属性的另类访问方式
    获取类所有属性和查看帮助文档
    类的私有属性及私方法(请注意属性的传值方式)
    类的私有属性及私方法
    类的定义
    怎么区分类变量和实例变量?
    面向对象编程案例04--访问控制
    面向对象编程案例03---继承之高级部分
    python 面向对象编程案例01
    静态方法
  • 原文地址:https://www.cnblogs.com/llllllpppppp/p/7562724.html
Copyright © 2020-2023  润新知