zoukankan      html  css  js  c++  java
  • 最短路

    最短路

    一、求出最短路径的长度

      以下没有特别说明的话,dis[u][v]表示从uv最短路径长度,w[u][v]表示连接uv的边的长度。

    1.Floyed-Warshall算法 O(N3)

      简称Floyed(弗洛伊德)算法,是最简单的最短路径算法,可以计算图中任意两点间的最短路径。Floyed的时间复杂度是O (N3),适用于出现负边权的情况。

    算法描述:

           初始化:点uv如果有边相连,则dis[u][v]=w[u][v]

      如果不相连则dis[u][v]=0x7fffffff

    For (k = 1; k <= n; k++)

        For (i = 1; i <= n; i++)

     For (j = 1; j <= n; j++)

         If (dis[i][j] >dis[i][k] + dis[k][j])

             dis[i][j] = dis[i][k] + dis[k][j];

            算法结束:dis[i][j]得出的就是从ij的最短路径。

    算法分析&思想讲解:

      三层循环,第一层循环中间点k,第二第三层循环起点终点ij,算法的思想很容易理解:如果点i到点k的距离加上点k到点j的距离小于原先点i到点j的距离,那么就用这个更短的路径长度来更新原先点i到点j的距离。

      在上图中,因为dis[1][3]+dis[3][2]<dis[1][2],所以就用dis[1][3]+dis[3][2]来更新原先12的距离。

      我们在初始化时,把不相连的点之间的距离设为一个很大的数,不妨可以看作这两点相隔很远很远,如果两者之间有最短路径的话,就会更新成最短路径的长度。Floyed算法的时间复杂度是O(N3) Floyed算法变形:

      如果是一个没有边权的图,把相连的两点间的距离设为dis[i][j]=true,不相连的两点设为dis[i][j]=false,用Floyed算法的变形:

    For (k = 1; k <= n; k++)

      For (i = 1; i <= n; i++)

        For (j = 1; j <= n; j++)

        dis[i][j] = dis[i][j] || (dis[i][k] && dis[k][j]);

           用这个办法可以判断一张图中的两点是否相连。

    最后再强调一点:用来循环中间点的变量k必须放在最外面一层循环。

    2.Dijkstra算法O (N2)

    用来计算从一个点到其他所有点的最短路径的算法,是一种单源最短路径算法。也就是说,只能计算起点只有一个的情况。

    Dijkstra的时间复杂度是O (N2),它不能处理存在负边权的情况。

    算法描述:

           设起点为sdis[v]表示从sv的最短路径,pre[v]v的前驱节点,用来输出路径。

           1)初始化:dis[v]=(vs); dis[s]=0; pre[s]=0;

           2For (i = 1; i <= n ; i++)

                1.在没有被访问过的点中找一个顶点u使得dis[u]是最小的。

                2.u标记为已确定最短路径

                3.For u相连的每个未确定最短路径的顶点v

                  if  (dis[u]+w[u][v] < dis[v])

                   {dis[v] = dis[u] + w[u][v];pre[v] = u;}

           3)算法结束:dis[v]sv的最短距离;pre[v]v的前驱节点,用来输出路径。

    算法分析&思想讲解:

    从起点到一个点的最短路径一定会经过至少一个“中转点”(例如下图15的最短路径,中转点是2。特殊地,我们认为起点1也是一个“中转点”)。显而易见,如果我们想求出起点到一个点的最短路径,那我们必然要先求出中转点的最短路径(例如我们必须先求出点2 的最短路径后,才能求出从起点到5的最短路径)。

    换句话说,如果起点1到某一点V0的最短路径要经过中转点Vi,那么中转点Vi一定是先于V0被确定了最短路径的点。

     我们把点分为两类,一类是已确定最短路径的点,称为“白点”,另一类是未确定最短路径的点,称为“蓝点”。如果我们要求出一个点的最短路径,就是把这个点由蓝点变为白点。从起点到蓝点的最短路径上的中转点在这个时刻只能是白点。

     Dijkstra的算法思想,就是一开始将起点到起点的距离标记为0,而后进行n次循环,每次找出一个到起点距离dis[u]最短的点u,将它从蓝点变为白点。随后枚举所有的蓝点vi,如果以此白点为中转到达蓝点vi的路径dis[u]+w[u][vi]更短的话,这将它作为vi的“更短路径”dis[vi](此时还不确定是不是vi的最短路径)。

        就这样,我们每找到一个白点,就尝试着用它修改其他所有的蓝点。中转点先于终点变成白点,故每一个终点一定能够被它的最后一个中转点所修改,而求得最短路径。

    3.Bellman-Ford算法O(NE)

    简称Ford(福特)算法,同样是用来计算从一个点到其他所有点的最短路径的算法,也是一种单源最短路径算法。能够处理存在负边权的情况,但无法处理存在负权回路的情况(下文会有详细说明)。

    算法时间复杂度:O(NE)N是顶点数,E是边数。

    算法实现:

    s为起点,dis[v]即为sv的最短距离,pre[v]v前驱。w[j]是边j的长度,且j连接uv

    初始化:dis[s]=0,dis[v]=∞(vs),pre[s]=0

    For (i = 1; i <= n-1; i++)

    For (j = 1; j <= E; j++)        //注意要枚举所有边,不能枚举点。

    if (dis[u]+w[j]<dis[v])  //uv分别是这条边连接的两个点。

    {dis[v] =dis[u] + w[j];pre[v] = u;}

    算法分析&思想讲解:

    Bellman-Ford算法的思想很简单。一开始认为起点是白点(dis[1]=0),每一次都枚举所有的边,必然会有一些边,连接着白点和蓝点。因此每次都能用所有的白点去修改所有的蓝点,每次循环也必然会有至少一个蓝点变成白点。

    负权回路:

    虽然Bellman-Ford算法可以求出存在负边权情况下的最短路径,却无法解决存在负权回路的情况。

     负权回路是指边权之和为负数的一条回路,上图中----②这条回路的边权之和为-3。在有负权回路的情况下,从16的最短路径是多少?答案是无穷小,因为我们可以绕这条负权回路走无数圈,每走一圈路径值就减去3,最终达到无穷小。

    所以说存在负权回路的图无法求出最短路径,Bellman-Ford算法可以在有负权回路的情况下输出错误提示。

     如果在Bellman-Ford算法的两重循环完成后,还是存在某条边使得:dis[u]+w<dis[v],则存在负权回路:

    For每条边(u,v)

        If  (dis[u]+w<dis[v])  return False

    如果我们规定每条边只能走一次,在这个前提下可以求出负权回路的最短路径。这个问题就留待读者自己思考(提示:对Floyed做一点小处理)。

    4SPFA算法O(kE)

    SPFABellman-Ford算法的一种队列实现,减少了不必要的冗余计算。

    主要思想:

    初始时将起点加入队列。每次从队列中取出一个元素,并对所有与它相邻的点进行修改,若某个相邻的点修改成功,则将其入队。直到队列为空时算法结束。

    这个算法,简单的说就是队列优化的bellman-ford,利用了每个点不会更新次数太多的特点发明的此算法。

    SPFA 在形式上和广度优先搜索非常类似,不同的是广度优先搜索中一个点出了队列就不可能重新进入队列,但是SPFA中一个点可能在出队列之后再次被放入队列,也就是说一个点修改过其它的点之后,过了一段时间可能会获得更短的路径,于是再次用来修改其它的点,这样反复进行下去。

    算法时间复杂度:O(kE)E是边数。K是常数,平均值为2

    算法实现:

        dis[i]记录从起点si的最短路径,w[i][j]记录连接ij的边的长度。pre[v]记录前趋。team[1..n]为队列,头指针head,尾指针tail。布尔数组exist[1..n]记录一个点是否现在存在在队列中。

        初始化:dis[s]=0,dis[v]=∞(vs),memset(exist,false,sizeof(exist));

        起点入队team[1]=s; head=0; tail=1;exist[s]=true;

        do

        {

     1、头指针向下移一位,取出指向的点u

     2exist[u]=false;已被取出了队列

     3foru相连的所有点v          

    //注意不要去枚举所有点,用数组模拟邻接表存储

                  if (dis[v]>dis[u]+w[u][v])

                     {

                          dis[v]=dis[u]+w[u][v];pre[v]=u;

                         if (!exist[v]) //队列中不存在v点,v入队。

                           {//尾指针下移一位,v入队;exist[v]=true;}

                     

        }while (head < tail);

    循环队列:

      采用循环队列能够降低队列大小,队列长度只需开到2*n+5即可。例题中的参考程序使用了循环队列。

    二、输出最短路径

    1.单源最短路径的输出

    DijkstraBellman-FordSPFA都是单源最短路径算法,它们的共同点是都有一个数组pre[x] 用来记录从起点到x的最短路径中,x的前驱结点是哪个。每次更新,我们就给pre[x]赋一个新值,结合上面的思想讲解,相信对于记录某点的前驱结点是不难理解的。那么怎么利用pre[x]数组输出最短路径方案呢?

    感谢各位与信奥一本通的鼎力相助!

  • 相关阅读:
    用Visual C#实现文件下载
    解读C#中的规则表达式
    NET(C#)连接各类数据库集锦
    C#中编写多线程(1)——起步
    C#中的常用加密算法与其它语言的兼容性
    C#的事件处理机制应用
    TCP IP协议之通信详解
    手把手教你AspNetCore WebApi:认证与授权
    TCP IP协议之初识
    mysql中exit和in的区别
  • 原文地址:https://www.cnblogs.com/SeanOcean/p/10975719.html
Copyright © 2011-2022 走看看