• 浅谈数据结构-最短路径


    最短路径和最小生成树在应用很是不同的,比如:一开始修建一条地铁,然后在地铁点上有多个点,需要修建一个路程最短的地铁线,将这些地铁点连接起来,这就是最小生成树(点与点之间距离是已知的)。小强需要从A点去B点旅游,中间会经过好几个点,需要找出条最短路径到达B点。从应用上明显看出,两者的目的不同、初始化条件也是不同的。

    一、Dijkstra(迪杰斯特拉)算法

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

    1、算法思想

    令G = (V,E)为一个带权有向图,把图中的顶点集合V分成两组,第一组为已求出最短路径的顶点集合S(初始时S中只有源节点,以后每求得一条最短路径,就将它对应的顶点加入到集合S中,直到全部顶点都加入到S中);第二组是未确定最短路径的顶点集合U。在加入过程中,总保持从源节点v到S中各顶点的最短路径长度不大于从源节点v到U中任何顶点的最短路径长度。

    image

    针对上图建立的两个集合,之后运用Dijkstra算法运行后,两个集合中元素为将ABCDEF在数组中坐标为1,2,3,4,5,6:

    迭代 S U dist[2] dist[3] dist[4] dist[5] dist[6]
    1 A - 6 3 MAX MAX MAX
    2 A,C C 5 3 6 7 MAX
    3 A,C,B B 5 3 6 7 MAX
    4 A,C,B,D D 5 3 6 7 9
    5 A,C,B,D,E E 5 3 6 7 9
    6 A,B,C,D,E,F F 5 3 6 7 9

    2、算法分析

    根据上面分析,得知需要创建两个数组,创建顶点集合,还有边集合,用于保存点到各个边的权重,然后在权重集合选取最小的权值边对的顶点,然后继续循环。

    1. 创建顶点结合nNodeIndex,初始化为0,数组中为1是,表示对应的顶点已经添加到最短路径顶点集合S了。
    2. 创建初始顶点到各个顶点的边集合,保存此顶点到各个顶点的距离(权重),用邻接矩阵中行元素初始化(类似最小生成树)。
    3. 循环计算此顶点到各个顶点的最小值,得知后nNodeIndex[i] = 1,同时更新边集合 的数值。
    4. 总体上讲还是比较容易的。

    3、例图解释

    4、代码

    //Dijkstra算法
    void GraphData::ShortPath_Dijkstra(GraphArray *pArray)
    {
        //Dijkstra算法和最小生成树的算法,在某种程度是相似的
        int min,i,j,k;
        int nNodeIndex[MAXVEX];      //保存相关顶点坐标,1就是已经遍历访问的过结点(在最小生成树中为数值表示遍历过同时值与坐标是一条边)
        int nNodeWeight[MAXVEX];     //保存某个顶点到各个顶点的权值,为不为0和最大值表示遍历过了。
        int nPathLength[MAXVEX];     //坐标和元素表示为同时值和坐标表示一边,与Primes有相同的
        //两个数组的初始化
        printf("开始初始化,当前顶点边的权值为:");
        for(i = 0;i<pArray->numVertexes;i++)
        {
            nNodeIndex[i] = 0; 
            nNodeWeight[i] = pArray->arg[0][i];//设定在矩阵中第一个顶点为初始点。
            nPathLength[i] = 0;
            printf(" %c",nNodeWeight[i]);
        }
        nNodeWeight[0] = 0;  //选取坐标点0为起始点。
        nNodeIndex[0] = 1;  //这样0就是起始点,设为1.(和Prime的不同)
        //算法思想
        for (i = 1;i< pArray->numVertexes;i++)
        {
            min = INFINITY;    //初始化权值为最大值;
            j = 1;
            k = 0;
            // 循环全部顶点,寻找与初始点边权值最小的顶点,记下权值和坐标
            while(j < pArray->numVertexes)
            {
                //如果权值不为0,且权值小于min,为0表示本身
                if (!nNodeIndex[j]&&nNodeWeight[j] < min)  //这里Prime是权重中不为0,
                {
                    min     = nNodeWeight[j];
                    k = j;     //保存上述顶点的坐标值
                }
                j++;
            }
            printf("当前顶点边中权值最小边(%d,%d)
    ",nNodeIndex[k] , k); //打印当前顶点边中权值最小
            //nNodeWeight[k] = 0; //将当前顶点的权值设置为0,表示此顶点已经完成任务
            nNodeIndex[k] = 1;     //将目前找到的最近的顶点置为1
    
            //for (j = 1;j< pArray->numVertexes;j++)  //循环所有顶点,查找与k顶点的最小边
            //{
            //    //若下标为k的顶点各边权值小于此前这些顶点未被加入的生成树权值
            //    if (nNodeWeight[j] != 0&&pArray->arg[k][j] < nNodeWeight[j])
            //    {
            //        nNodeWeight[j] = pArray->arg[k][j];
            //        nNodeIndex[j] = k;     //将下标为k的顶点存入adjvex
            //    }
            //}
            //修正当前最短路径及距离
            for (j = 1;j< pArray->numVertexes;j++)  //循环所有顶点,查找与k顶点的最小边
            {
                //若下标为k的顶点各边权值小于此前这些顶点未被加入的生成树权值
                if (!nNodeIndex[j] && pArray->arg[k][j] + min< nNodeWeight[j])
                {
                    nNodeWeight[j] = pArray->arg[k][j] + min;
                    nPathLength[j] = k;     //将下标为k的顶点存入adjvex
                }
            }
            //打印当前顶点状况
            printf("坐标点数组为:");
            for(j = 0;j< pArray->numVertexes;j++)
            {
                printf("%3d ",nPathLength[j]);
            }
            printf("
    ");
            printf("权重数组为:");
            for(j = 0;j< pArray->numVertexes;j++)
            {
                printf("%3d ",nNodeWeight[j]);
            }
            printf("
    ");
        }
    
    }

    5、代码分析

    image

    上图就是图的邻接矩阵,对应上面的例图分析,我们分析下,代码运算结果。

    image

    上图是代码运算后的结果,首先是第一次,最小边为(0,2),加入的顶点是C。第二次最小边是(0,1),加入的结果是B。等等,会发现结果同前面例图分析结果一样。

    最后的权重数组为(0,5,3,6,7,9)意味着顶点A到B,C,D,E,F的距离是5,3,6,7,9。A->B:5,根据坐标点数组nPathLength[1] = 2,意味着经过坐标为2的顶点C,再看nPathLength[2] = 0,结束。再比如A->D:6,nPathLength[4] = 2,所以为A->C->D.同理其他路劲也是如此寻找。

    二、Floyd算法

    Dijkstra算法解决了某个源点到其余各个顶点的最短距离问题。从循环语句上判断,算法的时间复杂度是O(n2)。在循环的外面再加一个循环,也就成了所有顶点的最短距离。此时算法的复杂度就是O(n3).

    弗洛依德(Floyd)算法就是一个事件复杂度为O(n)的算法,只不过算法非常简洁优雅。

    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、算法分析

    1. 创建一个矩阵,记录两个顶点的权值。在DIjkstra算法中是记录一个顶点到其他顶点的路径长度,声明一个数组,此处是各个顶点,所以为矩阵。
    2. 创建一个矩阵,记录顶点到另个顶点路径的走法,这个后面会讲解。
    3. Floyd算法思想:对于每一对顶点 u 和 v,看看是否存在一个顶点 w 使得从 u 到 w 再到 v 比己知的路径更短。将权值鞠振宁更新,同样还有路径矩阵。

    3、例图解释

    (1)程序开始运行,第4-11行就是初始化了D和P,使得它们成为   上图    的两个矩阵。从矩阵也得到,v0->v1路径权值为1,v0->v2路径权值为5,v0->v3无边连线,所以路径权值为极大值65535。
        (2)第12~25行,是算法的主循环,一共三层嵌套,k代表的就是中转顶点的下标。v代表起始顶点,w代表结束顶点。
        (3)当k = 0时,也就是所有的顶点都经过v0中转,计算是否有最短路径的变化。可惜结果是,没有任何变化,如下图所示。

    (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时,也修改了一些数据,请看下图左图中虚线框数据。由于这些最小权值的修正,所以在路径矩阵P上,也要做处理,将它们都改为当前的P[v][k]值,见代码第21行。

      (5)接下来就是k = 2,一直到8结束,表示针对每个顶点做中转得到的计算结果,当然,我们也要清楚,D0是以D-1为基础,D1是以D0为基础,......,D8是以D7为基础的。最终,当k = 8时,两个矩阵数据如下图所示。

    至此,我们的最短路径就算是完成了。可以看到矩阵第v0行的数值与迪杰斯特拉算法求得的D数组的数值是完全相同。而且这里是所有顶点到所有顶点的最短路径权值和都可以计算出。
        那么如何由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,说明要经过3,........,这样很容易就推倒出最终的最短路径值为v0->v1->v2->v4->v3->v6->v7->v8。

    4、示例代码

    //Floyd算法
    void GraphData::ShortPath_Floyd(GraphArray *pArray)
    {
        int i,j,m,k;
        int nNodeIndex[MAXVEX][MAXVEX];
        int nNodeWeight[MAXVEX][MAXVEX];
        
        for ( i = 0;i< pArray->numVertexes;i++)
        {
            for (j = 0;j< pArray->numVertexes;j++)
            { 
                nNodeIndex[i][j] = j;                         /* 初始化 */
                nNodeWeight[i][j] = pArray->arg[i][j];  /* [i][j]值即为对应点间的权值 */
            }
        }
        for (i = 0;i< pArray->numVertexes;i++)
        {
            for (j = 0;j< pArray->numVertexes;j++)
            {
                for (k = 0;k<pArray->numVertexes;k++)
                {
                    if (pArray->arg[j][k] > pArray->arg[j][i] + pArray->arg[i][k])
                    {
                        /* 如果经过下标为k顶点路径比原两点间路径更短 */
                        nNodeWeight[j][k] = pArray->arg[j][i] + pArray->arg[i][k];  /* 将当前两点间权值设为更小的一个 */
                        nNodeIndex[j][k] = nNodeIndex[j][i];  /* 路径设置为经过下标为k的顶点 */
                    }
                }
            }
        }
        for (i = 0; i< pArray->numVertexes;i++)
        {
            for (j = 0;j< pArray->numVertexes;j++)
            {
                printf("v%d-v%d weight: %d",i,j,nNodeWeight[i][j]);
                    m = nNodeIndex[i][j];    //获得第一个路径点的顶点下标
                printf("path :%d",i);    //打印源点
                while(m!=j)
                {
                    printf(" -> %d",m);   //打印路径顶点
                    m = nNodeIndex[m][j];   //获取下一个路径顶点下标
                }
                printf(" -> %d
    ",m);    //打印路径终点。
            }
    
            printf("
    ");
        }
    }

    5、代码分析

    image

    同样对于Dijkstra算法中,同样的邻接矩阵,我们最后发现其中v0到各个顶点数据与Dijkstra中数据一样奥,同时显示出路径中通过的顶点。

  • 相关阅读:
    [LeetCode] 143. 重排链表
    [LeetCode] 342. 4的幂
    [LeetCode] 1744. 你能在你最喜欢的那天吃到你最喜欢的糖果吗?
    [LeetCode] 148. 排序链表
    [LeetCode] 525. 连续数组
    [LeetCode] 160. 相交链表
    [LeetCode] 134. 加油站
    [LeetCode] 474. 一和零
    CentOS 升级 OpenSSH
    AWS 证书取消挂靠
  • 原文地址:https://www.cnblogs.com/polly333/p/4767051.html
Copyright © 2020-2023  润新知