数据结构与算法-最短路径

我们时常会面临着对路径选择的决策问题。例如在北京、上海、广州等城市,因其城市面积较大,乘地铁或公交都要考虑从A点到B点,如何换乘到达?

现实中,每个人需求不同,选择方案就不尽相同。有人为了省钱,它需要的是路程最短(定价以路程长短为标准),但可能由于线路班次少,换乘站间距离长等原因并不省时间;而另一些人,为了要赶飞机火车或者早晨上班不迟到,他最大的需求是总时间要短;还有一类人,如老人行动不便,或者上班族下班,忙碌一天累得要死,他们都不想多走路,哪怕车子绕远路耗时长也无所谓,关键是换乘要少,这样可以在车上好好休息一下(有些线路方案换乘两次比换乘三四次耗时还长)。这些都是老百姓的需求,简单的图形可以靠人的经验和感觉,但复杂的道路或地铁网就需要计算机通过算法计算来提供最佳的方案。我们今天就要来研究关于图的最短路径的问题。

在网图和非网图中,最短路径的含义是不同的。由于非网图它没有边上的权值,所谓的最短路径,其实就是指两顶点之间经过的边数最少的路径;而对于网图来说,最短路径,是指两顶点之间经过的边上权值之和最少的路径,并且我们称路径上的第一个顶点是源点,最后一个顶点是终点。显然,我们研究网图更有实际意义,就地图来说,距离就是两顶点间的权值之和。而非网图完全可以理解为所有的边的权值都为1的网。

我们要讲解两种求最短路径的算法。先来讲第一种,从某个源点到其余各顶点的最短路径问题。

image-20200510211229875

迪杰斯特拉(Dijkstra)算法

这是一个按路径长度递增的次序产生最短路径的算法。它的思路大体是这样的。

比如说要求下图中顶点v0到顶点v1的最短距离,没有比这更简单的了,答案就是1,路径就是直接v0连线到v1。

image-20200510212008693

由于顶点v1还与v2、v3、v4连线,所以此时我们同时求得了v0→v1→v2=1+3=4,v0→v1→ v3=1+7=8,v0→v1→v4=1+5=6。

现在,我问v0到v2的最短距离,如果你不假思索地说是5,那就犯错了。因为边上都有权值,刚才已经有v0→v1→v2的结果是4,比5还要小1个单位,它才是最短距离,如下图所示。

image-20200510212237989

由于顶点v2还与v4、v5连线,所以此时我们同时求得了v0→v2→v4其实就是v0→v1→v2→v4=4+1=5,v0→v2→v5=4+7=11。这里v0→v2我们用的是刚才计算出来的较小的4。此时我们也发现v0→v1→v2→v4=5要比v0→v1→v4=6还要小。所以v0到v4目前的最小距离是5,如下图所示。

image-20200510212341892

当我们要求v0到v3的最短距离时,通向v3的三条边,除了v6没有研究过外,v0→v1→v3的结果是8,而v0→v4→v3=5+2=7。因此,v0到v3的最短距离是7,如下图所示。

image-20200510212412545

它并不是一下子就求出了v0到v8的最短路径,而是一步步求出它们之间顶点的最短路径,过程中都是基于已经求出的最短路径的基础上,求得更远顶点的最短路径,最终得到你要的结果。

#define MAXVEX 9
#define INFINITY 65535
typedef int 
/* 用于存储最短路径下标的数组 */
Patharc[MAXVEX];                       
typedef int 
/* 用于存储到各点最短路径的权值和 */
ShortPathTable[MAXVEX];                
/* Dijkstra算法,求有向网G的v0顶点到其余顶点v最短路径P[v]及带权长度D[v] */
/* P[v]的值为前驱顶点下标,D[v]表示v0到v的最短路径长度和。 */
void ShortestPath_Dijkstra(MGraph G, int v0, Patharc *P, ShortPathTable *D)
{
    int v, w, k, min;
    /* final[w]=1表示求得顶点v0至vw的最短路径 */
    int final[MAXVEX];                          
    /* 初始化数据 */
    for (v = 0; v < G.numVertexes; v++)         
    {
        /* 全部顶点初始化为未知最短路径状态 */
        final[v] = 0;                           
        /* 将与v0点有连线的顶点加上权值 */
        (*D)[v] = G.arc[v0][v];                 
        /* 初始化路径数组P为-1 */
        (*P)[v] = -1;                           
    }
    /* v0至v0路径为0 */
    (*D)[v0] = 0;                               
    /* v0至v0不需要求路径 */
    final[v0] = 1;                              
    /* 开始主循环,每次求得v0到某个v顶点的最短路径 */
    for (v = 1; v < G.numVertexes; v++)
    {
        /* 当前所知离v0顶点的最近距离 */
        min=INFINITY;                           
        /* 寻找离v0最近的顶点 */
        for (w = 0; w < G.numVertexes; w++)     
         {
            if (!final[w] && (*D)[w] < min)
            {
                k=w;
                /* w顶点离v0顶点更近 */
                min = (*D)[w];                  
            }
        }
        /* 将目前找到的最近的顶点置为1 */
        final[k] = 1;                           
        /* 修正当前最短路径及距离 */
        for (w = 0; w < G.numVertexes; w++)     
        {   
            /* 如果经过v顶点的路径比现在这条路径的长度短的话 */
            if (!final[w] && (min + G.arc[k][w] < (*D)[w]))
            {                                   
                /* 说明找到了更短的路径,修改D[w]和P[w] */
                /* 修改当前路径长度 */
                (*D)[w] = min + G.arc[k][w];    
                (*P)[w]=k;
            }
        }
    }
}

调用此函数前,其实我们需要为下图的左图准备邻接矩阵MGraph的G,如下图的右图,并且定义参数v0为0。

image-20200510212610766

1.程序开始运行,第4行final数组是为了v0到某顶点是否已经求得最短路径的标记,如果v0到vw已经有结果,则final[w]=1。

2.第5~10行,是在对数据进行初始化的工作。此时final数组值均为0,表示所有的点都未求得最短路径。D数组为{65535,1,5,65535,65535,65535,65535,65535,65535}。因为v0与v1和v2的边权值为1和5。P数组全为0,表示目前没有路径。

3.第11行,表示v0到v0自身,权值和结果为0。D数组为{0,1,5,65535,65535,65535,65535,65535,65535}。第12行,表示v0点算是已经求得最短路径,因此final[0]=1。此时final数组为{1,0,0,0,0,0,0,0,0}。此时整个初始化工作完成。

4.第13~33行,为主循环,每次循环求得v0与一个顶点的最短路径。因此v从1而不是0开始。

5.第15~23行,先令min为65535的极大值,通过w循环,与D[w]比较找到最小值min=1,k=1。

6.第24行,由k=1,表示与v0最近的顶点是v1,并且由D[1]=1,知道此时v0到v1的最短距离是1。因此将v1对应的final[1]设置为1。此时final数组为{1,1,0,0,0,0,0,0,0}。

7.第25~32行是一循环,此循环甚为关键。它的目的是在刚才已经找到v0与v1的最短路径的基础上,对v1与其他顶点的边进行计算,得到v0与它们的当前最短距离,如下图所示。因为min=1,所以本来D[2]=5,现在v0→v1→v2=D[2]=min+3=4,v0→v1→v3=D[3]=min+7=8,v0→v1→v4=D[4]=min+5=6,因此,D数组当前值为{0,1,4,8,6,65535,65535,65535,65535}。而P[2]=1,P[3]=1,P[4]=1,它表示的意思是v0到v2、v3、v4点的最短路径它们的前驱均是v1。此时P数组值为:{0,0,1,1,1,0,0,0,0}。

image-20200510213137197

8.重新开始循环,此时v=2。第15~23行,对w循环,注意因为final[0]=1和fi-nal[1]=1,由第18行的!final[w]可知,v0与v1并不参与最小值的获取。通过循环比较,找到最小值min=4,k=2。

9.第24行,由k=2,表示已经求出v0到v2的最短路径,并且由D[2]=4,知道最短距离是4。因此将v2对应的final[2]设置为1,此时final数组为:{1,1,1,0,0,0,0,0,0}。10.第25~32行。在刚才已经找到v0与v2的最短路径的基础上,对v2与其他顶点的边,进行计算,得到v0与它们的当前最短距离,如下图所示,因为min=4,所以本来D[4]=6,现在v0→v2→v4=D[4]=min+1=5,v0→v2→v5=D[5]=min+7=11,因此,D数组当前值为:{0,1,4,8,5,11,65535,65535,65535}。而原本P[4]=1,此时P[4]=2,P[5]=2,它表示v0到v4、v5点的最短路径它们的前驱均是v2。此时P数组值为:{0,0,1,1,2,2,0,0,0}。

image-20200510213340380

11.重新开始循环,此时v=3。第15~23行,通过对w循环比较找到最小值min=5,k=4。

12.第24行,由k=4,表示已经求出v0到v4的最短路径,并且由D[4]=5,知道最短距离是5。因此将v4对应的final[4]设置为1。此时final数组为:{1,1,1,0,1,0,0,0,0}。

13.第25~32行。对v4与其他顶点的边进行计算,得到v0与它们的当前最短距离,如下图所示。因为min=5,所以本来D[3]=8,现在v0→v4→v3=D[3]=min+2=7,本来D[5]=11,现在v0→v4→v5=D[5]=min+3=8,另外v0→v4→v6=D[6]=min+6=11,v0→v4→v7=D[7]=min+9=14,因此,D数组当前值为:{0,1,4,7,5,8,11,14,65535}。而原本P[3]=1,此时P[3]=4,原本P[5]=2,此时P[5]=4,另外P[6]=4,P[7]=4,它表示v0到v3、v5、v6、v7点的最短路径它们的前驱均是v4。此时P数组值为:{0,0,1,4,2,4,4,4,0}。

image-20200510213509011

14.之后的循环就完全类似了。得到最终的结果,如下图所示。此时final数组为:{1,1,1,1,1,1,1,1,1},它表示所有的顶点均完成了最短路径的查找工作。此时D数组为:{0,1,4,7,5,8,10,12,16},它表示v0到各个顶点的最短路径数,比如D[8]=1+3+1+2+3+2+4=16。此时的P数组为:{0,0,1,4,2,4,3,6,7},这“串数字可能略为难理解一些。比如P[8]=7,它的意思是v0到v8的最短路径,顶点v8的前驱顶点是v7,再由P[7]=6表示v7的前驱是v6,P[6]=3,表示v6的前驱是v3。这样就可以得到,v0到v8的最短路径为v8←v7←v6←v3←v4←v2←v1←v0,即v0→v1→v2→v4→v3→v6→v7→v8。

image-20200510213558138

其实最终返回的数组D和数组P,是可以得到v0到任意一个顶点的最短路径和路径长度的。例如v0到v8的最短路径并没有经过v5,但我们已经知道v0到v5的最短路径了。由D[5]=8可知它的路径长度为8,由P[5]=4可知v5的前驱顶点是v4,所以v0到v5的最短路径是v0→v1→v2→v4→v5。

也就是说,我们通过迪杰斯特拉(Dijkstra)算法解决了从某个源点到其余各顶点的最短路径问题。从循环嵌套可以很容易得到此算法的时间复杂度为O(n2),尽管有同学觉得,可不可以只找到从源点到某一个特定终点的最短路径,其实这个问题和求源点到其他所有顶点的最短路径一样复杂,时间复杂度依然是O(n2)。

这就好比,你吃了七个包子终于算是吃饱了,就感觉很不划算,前六个包子白吃了,应该直接吃第七个包子,于是你就去寻找可以吃一个就能饱肚子的包子,能够满足你的要求最终结果只能有一个,那就是用七个包子的面粉和馅做的一个大包子。这种只关注结果而忽略过程的思想是非常不可取的。

可如果我们还需要知道如v3到v5、v1到v7这样的任一顶点到其余所有顶点的最短路径怎么办呢?此时简单的办法就是对每个顶点当作源点运行一次迪杰斯特拉(Dijkstra)算法,等于在原有算法的基础上,再来一次循环,此时整个算法的时间复杂度就成了O(n3)。

弗洛伊德(Floyd)算法

下图的左图是一个最简单的3个顶点连通网图。

image-20200511085438240

我们先定义两个二维数组D[3][3]和P[3][3],D代表顶点到顶点的最短路径权值和的矩阵。P代表对应顶点的最小路径的前驱矩阵,用来存储路径。在未分析任何顶点之前,我们将D命名为D-1,其实它就是初始的图的邻接矩阵。将P命名为P-1,初始化为图中所示的矩阵。

因为只有三个顶点,因此需要查看v1→v0→v2,得到D-1[1][0]+D-1[0][2]=2+1=3。D-1[1][2]表示的是v1→v2的权值为5,我们发现D-1[1][2]>D-1[1][0]+D-1[0][2],通俗的话讲就是v1→v0→v2比直接v1→v2距离还要近。所以我们就让D-1[1][2]=D-1[1][0]+D-1[0][2]=3,同样的D-1[2][1]=3,于是就有了D0的矩阵。因为有变化,所以P矩阵对应的P-1[1][2]和P-1[2][1]也修改为当前中转的顶点v0的下标0,于是就有了P0。也就是说D0[v][w]=min{D-1[v][w],D-1[v][0]+D-1[0][w]}

接下来,其实也就是在D0和P0的基础上继续处理所有顶点经过v1和v2后到达另一顶点的最短路径,得到D1和P1、D2和P2完成所有顶点到所有顶点的最短路径计算工作。

首先我们针对下图的左网图准备两个矩阵D-1和P-1,D-1就是网图的邻接矩阵,P-1初设为P[i][j]=j这样的矩阵,它主要用来存储路径。

image-20200511090422033

代码如下,注意因为是求所有顶点到所有顶点的最短路径,因此Pathmatirx和ShortPathTable都是二维数组。

typedef int Pathmatirx[MAXVEX][MAXVEX];
typedef int ShortPathTable[MAXVEX][MAXVEX];
/* Floyd算法,求网图G中各顶点v到其余顶点w最短
   路径P[v][w]及带权长度D[v][w] */
void ShortestPath_Floyd(MGraph G, Pathmatirx *P, 
                        ShortPathTable *D)
{
    int v, w, k;
    /* 初始化D与P */
    for (v = 0; v < G.numVertexes; ++v)         
       for (w = 0; w < G.numVertexes; ++w)
        {
           /* D[v][w]值即为对应点间的权值 */
           (*D)[v][w] = G.matirx[v][w];        
           /* 初始化P */
           (*P)[v][w] = w;                     
        }
    }
    for (k = 0; k < G.numVertexes; ++k)
    {
        for (v = 0; v < G.numVertexes; ++v)
        {
            for (w = 0; w < G.numVertexes; ++w)
            {
                if ((*D)[v][w] > (*D)[v][k] + (*D)[k][w])
                {                               
                    /* 如果经过下标为k顶点路径比原两点间路径更短 */
                    /* 将当前两点间权值设为更小的一个 */
                    (*D)[v][w] = (*D)[v][k] + (*D)[k][w];
                      /* 路径设置经过下标为k的顶点 */
                    (*P)[v][w] = (*P)[v][k];    
                }
            }
        }
    }
}

1.程序开始运行,第4~11行就是初始化了D和P,使得它们成为上图的两个矩阵。从矩阵也得到,v0→v1路径权值是1,v0→v2路径权值是5,v0→v3无边连线,所以路径权值为极大值65535。

2.第12~25行,是算法的主循环,一共三层嵌套,k代表的就是中转顶点的下标。v代表起始顶点,w代表结束顶点。

3.当K=0时,也就是所有的顶点都经过v0中转,计算是否有最短路径的变化。可惜结果是,没有任何变化,如下图所示。

image-20200511090653490

4.当K=1时,也就是所有的顶点都经过v1中转。此时,当v=0时,原本D[0][2]=5,现在由于D[0][1]+D[1][2]=4。因此由代码的第20行,二者取其最小值,得到D[0][2]=4,同理可得D[0][3]=8、D[0][4]=6,当v=2、3、4时,也修改了一些数据,请参考如x下图5左图中虚线框数据。由于这些最小权值的修正,所以在路径矩阵P上,也要作处理,将它们都改为当前的P[v][k]值,见代码第21行。

image-20200511090958807

5.接下来就是k=2一直到8结束,表示针对每个顶点做中转得到的计算结果,当然,我们也要清楚,D0是以D-1为基础,D1是以D0为基础,……,D8是以D7为基础,就像我们曾经说过的七个包子的故事,它们是有联系的,路径矩阵P也是如此。最终当k=8时,两矩阵数据如下图所示。

image-20200511091037438

至此,我们的最短路径就算是完成了,你可以看到矩阵第v0行的数值与迪杰斯特拉(Dijkstra)算法求得的D数组的数值是完全相同,都是{0,1,4,7,5,8,10,12,16}。而且这里是所有顶点到所有顶点的最短路径权值和都可以计算出。

那么如何由P这个路径数组得出具体的最短路径呢?以v0到v8为例,从上图的右图第v8列,P[0][8]=1,得到要经过顶点v1,然后将1取代0得到P[1][8]=2,说明要经过v2,然后将2取代1得到P[2][8]=4,说明要经过v4,然后将4取代2得到P[4][8]=3,说明要经过v3,……,这样很容易就推导出最终的最短路径值为v0→v1→v2→v4→v3→v6→v7→v8。

求最短路径的显示代码可以这样写。

for (v = 0; v < G.numVertexes; ++v)
{
    for (w = v + 1; w < G.numVertexes; w++)
    {
        printf("v%d-v%d weight: %d ", v, w, D[v][w]);
        /* 获得第一个路径顶点下标 */
        k = P[v][w];                
        /* 打印源点 */
        printf(" path: %d", v);     
        /* 如果路径顶点下标不是终点 */
        while (k != w)              
        {
            /* 打印路径顶点 */
            printf(" -> %d", k);    
            /* 获得下一个路径顶点下标 */
            k = P[k][w];            
        }
        /* 打印终点 */
        printf(" -> %d\n", w);      
    }
    printf("\n");
}

你可能感兴趣的:(数据结构与算法-最短路径)