数据结构知识整理 - 应用图(一)- 最小生成树与最短路径

版权声明: https://blog.csdn.net/Ha1f_Awake/article/details/84618233

主要内容

普利姆算法(加点法)

克鲁斯卡尔算法(加边法)

迪杰斯特拉算法(单源点)

弗洛伊德算法(顶点对)


现实生活中的许多问题都可以转化为图来解决。例如,如何以最小成本构建一个通信网络,如何计算地图中两地之间的最短路径,如何为复杂活动中各子任务的完成寻找一个较优的顺序等。

下面将介绍图的四个常用算法:最小生成树最短路径拓扑排序关键路径


最小生成树

假设要在n个城市之间建立通信联络网,则连通n个城市只需要n-1条线路。这时,自然会考虑如何在最省经费的前提下完成任务。

在一个连通网的所有生成树中,各边的代价之和最小的那棵生成树称为该连通网的最小生成树

MST性质:最小生成树中必定存在一条具有最小权值的边。普利姆(Prim)算法克鲁斯卡尔(Kruskal)算法是两个利用MST性质构成最小生成树的算法。

普利姆算法的核心思想是归并点,时间复杂度为O(n²),适用于稠密网;

克鲁斯卡尔算法的核心思想是归并边,时间复杂度为O(elog2e),使用与稀疏网。

普利姆算法(加点法)

<逻辑思路>

(1)设所有顶点保存在集合V中,已被归并的点保存在集合U中,则未被归并的点保存在集合V-U中;

(2)在图中任意找一个起始顶点v1,v1归入U,离开V-U;

(3)顶点v1存在v2,v3,v4三个邻接顶点,找出权值最小的边(v1,v3);

(4)顶点v3归入U,离开V-U;

(5)顶点v1剩余邻接顶点v2,v4,顶点v3有邻接顶点v2,v4,v5,v6;

(6)比较(v1,v2)和(v3,v2),得出权值更小边(v3,v2);比较(v1,v4)和(v3,v4),两边权值相同;

*在逻辑思路中,其实第(6)步可以省略,直接比较所有边的权值,再从中选择权值最小的边。但代码实现中,应该避免数据冗余,先筛选部分意义重合的数据。

(7)比较(v3,v2),(v1,v4)或(v3,v4),(v3,v5),(v3,v6),找出权值最小的边(v3,v6);

(8)顶点v6归入U,离开V-U;

(9)顶点v1剩余邻接顶点v4(因为(v3,v2)的权值更小,所以不再需要考虑v1到v2的情况),顶点v3剩余邻接顶点v2,v4,v5,顶点v6有邻接顶点v4,v5;

(10)到这里思路应该清晰了。

<实现思路>——以邻接矩阵为存储结构的无向网

(1)顶点集合为V等价于邻接矩阵图中用于存储顶点信息的一维数组vexs[vexnum]

(2)算法最巧妙的地方——结构体数组closedge[vexnum],包含信息:最小边在集合U中的那个顶点(adjvex)和最小边的权值(lowcost)。

结构体数组closedge[]的使用正是<逻辑思路>中步骤(6)的体现。

closedge[vi-1]表示顶点vi,当lowcast不为0时,vi在集合V-U中;当lowcast记为0时,vi归并到集合U中

(3)循环执行某一段代码,直至closedge[]中所有元素的lowcast属性都归0,即所有顶点都并入到集合U中。

(看代码前可以先回顾下“邻接矩阵”的知识)

typedef struct                        /*定义结构体数组closedge[vexnum]*/
{
    Vextype adjvex;
    Arctype lowcast;
} closedge[vexnum];
    

void MiniSpanTree_Prim(AMGraph G, Vextype vi)
{
    int i = LocateVex(G, vi);         /*确定起始顶点vi的编号*/

    closedge[i] = {NULL, 0};          /*将vi归并到集合U中*/
    
    for(int vj = 1; vj <= G.vexnum; vj++)    /*对于V-U中的每个顶点vj,初始化closedge[vj-1]*/
    {
        int j = LocateVex(G, vj);

        if(j != i) closedge[j] = {vi, G.arcs[i][j]};
    }

    for(int k = 1; k < G.vexnum; k ++)       /*直到所有顶点归并到集合U前,循环执行某一段代码*/
    {
        i = Min(closedge);            /*函数Min()找出closedge[]中lowcast最小的元素,并返回下标i*/
                                      /*即找出权值最小的边,并找出位于V-U中的顶点vj*/
    closedge[i].lowcast = 0;          /*将顶点vj归并到集合U中*/

    u0 = closedge[i].adjvex           /*u0为最小边在U中的点*/
    v0 = G.vexs[i];                   /*v0为最小边在V-U中的点*/
    cout<<u0<<v0;                     /*输出u0,v0以记录路线*/

    for(int j = 0; j < G.vexnum; j++)
    {
        /*之前对closedge[]的元素进行过初始化,并入新的点后要重新选出权值更小的边,对应<逻辑思路>的步骤(6)*/
        if(G.arcs[i][j] < closedge[j].lowcast)    
            closedge[j] = {G.vexs[i], G.arcs[i][j]};  
    }
}

克鲁斯卡尔算法(加边法)

如果说普利姆算法是“加点法”,那么克鲁斯卡尔算法就是“加边法”。

<逻辑思路>

(1)将由n个顶点组成的连通图拆分成n个连通分量,即每个顶点为一个连通分量;

(2)将图上的所有边按权值排序;

(3)从最小边开始操作,归并边判断条件是下一条被选边不能使连通分量形成回路,即被选边的两个顶点head和tail不能在同一个连通分量上

(4)在循环中执行某段代码,直至所有顶点被归并到同一连通分量。

<实现思路>——以邻接矩阵为存储结构的无向网

(1)按权值排序可以使用“冒泡法”“选择法”

(2)从步骤(3)可以看出,我们需要类似于普利姆算法中的closedge[]那样的辅助数组。包含的信息:每一条边的头顶点和尾顶点以及边上的权值;

(3)同时,我们还需要一个标记数组辅助判断每一个顶点所属的连通分量

(4)归并一个顶点后,将顶点的连通分量改为并入它的连通分量。

typedef struct                         /*结构体数组的各元素代表各边*/
{
    Vextype head;
    Vextype tail;
    Arctype lowcast;
} Arcs[arcnum];

int VexSet[vexnum];                    /*VexSet[vi-1] = i;表示vi所在的连通分量编号为i,即它本身*/

void MiniSpanTree_Kruskal(AMGraph G)
{
    for(int t = 0; t < arcnum; t++)    /*输入各边信息*/
        cin>>Arcs[t].head>>Arcs[t].tail>>Arcs[t].lowcast;

    Sort(Arcs);                        /*按权值将图上各边从小到大排序*/

    for(int t = 0; t < arcnum; t++)    /*对图上所有边(权值从小到大)依次进行操作*/
    {
        Headv = LocateVex(G, Arcs[t].head);        /*确定头尾顶点的编号*/
        Tailv = LocateVex(G, Arcs[t].tail);
        
        VS_h = VexSet[HeadV];                      /*确定头尾顶点所在的连通分量*/
        VS_t = VexSet[Tailv];

        if(VS_h != VS_t)                           /*若两个顶点不在同一连通分量*/
        {
            cout<<Arcs[t].head<<Arcs[t].tail;      /*输出符合要求的边*/

            for(int v = 0; v < G.vexnum; v++)      /*对所有顶点进行判断*/
                if(VexSet[v] == VS_t)
                    VexSet[v] = VS_H;              /*将顶点归并到同一连通分量*/
        }
    }
}
    

最短路径

最简单的最短路径是求中转次数最少的路径,而不考虑每条边的权值。而在实际问题中,路径长度的度量就不再是路径上的边数,而是路径上所有边的权值之和

有向网中,习惯上称路径的第一个顶点为源点(Source),最后一个顶点为终点(Destination)

下面主要讨论两种最常见的最短路径问题:

一、从某个源点到其余顶点的最短路径;

二、求每一对顶点之间的最短路径。

迪杰斯特拉(Dijkstra)算法用于求解第一种问题,时间复杂度为O(n²)弗洛伊德(Floyd)算法用于求解第二种问题,时间复杂度为O(n³)

从实现形式上来说,弗洛伊德算法比迪杰斯特拉算法更为简洁

迪杰斯特拉算法(单源点)

<逻辑思路>

(1)从单源点出发,求到各个顶点的最短路径。该问题类似于就有向连通网最小生成树

(2)因此同样需要辅助数组来记录顶点是否被归并最短路径长度

(3)迪杰斯特拉算法的巧妙之处就在于设计了三个辅助数组:

1. 一维数组S[]:记录顶点vi是否被确定最短路径长度,即该顶点是否被归并到终点集合中。初始化:S[v0-1] = TRUE;

2. 一维数组Path[]:记录顶点vi的直接前驱。譬如存在最短路径<v1,v4,v3>,v4的直接前驱是v1,v3的直接前驱是v4,这样一来,依靠直接前驱数组就能将路径连接起来。初始化:若源点v0到vi有弧,则Path[vi-1]为v0-1(编号/下标);否则为-1。

3. 一维数组D[]:记录从源点v0到终点vi的最短路径长度初始化:若源点v0到vi有弧,则D[vi-1]为弧上的权值;否则为∞。

(4)算法开始时,先找到D[]上的最小值,然后并入第一个终点v1,并将S[v1-1]的值设为TRUE;

(5)由于加入了终点v1,相当于v0多了一个中转点,所以需要对D[]上的值进行更新;

(6)更新后继续找D[]上的最小值,继而找到了顶点v2,因为S[v2-1]的值为FALSE,即v2未被归并到终点集合,所以可以将它并入;

(7)若到v2的最短路径是<v0,v1,v2>,则将v2的直接前驱改为v1,即Path[v2-1] = v1-1

(8)反复执行以上过程(n-1次),直至所有顶点被并入终点集合

<实现思路>——以邻接矩阵为存储结构的有向网

(1)对应<逻辑思路>步骤(3)的初始化:

1. S[v0-1] = TRUE;

2. D[vi-1] = G.arcs[v0-1][vi-1];

3. if(G.arcs[v0-1][vi-1] != MaxInt) Path[vi-1] = v0-1;              /*MaxInt表示无穷∞,值为326767*/

(2)对应<逻辑思路>步骤(8)的n-1次循环

1. 步骤(4):D[v1]  = Min(D);

2. 步骤(6):if(S[v1-1] = FALSE) S[v1-1] = TRUE;

3. 步骤(5)和步骤(7): if(D[v1-1] + G.arcs[v1-1][v2-1] < D[v2-1])  {D[v2-1] = D[v1-1] + G.arcs[v1-1][v2-1]; Path[v2-1] = v1-1;}

#define TRUE 1
#define FALSE 0

void ShortestPath_DIJ(AMGraph G, int v0)
{
    int S[vexnum];
    int Path[vexnum];
    int D[vexnum];

    int i = LocateVex(G, v0);

    for(int vi = 1; vi <= G.vexnum; vi++) /*初始化*/
    {
        int j  =LocateVex(G, vi);

        S[j] = FALSE;

        D[j] = G.arcs[i][j];

        if(D[j] < MaxInt) Path[j] = i;    /*小于MaxInt即v0与vi间存在弧*/
        else Path[j] = -1;                /*若vi的直接前驱不是v0,则置为-1*/
    }
    
    S[i] = TRUE;
    D[i] = 0;                             /*源点到源点的路径长度为0*/

/*----------初始化结束-----------*/

    for(int t = 1; t < G.vexnum; t++)     /*循环n-1次*/
    {
        int v;                            /*下一个终点*/

        for(vi = 1; vi <= G.vexnum; vi++)
        {
            int min = MaxInt;             /*min保存最小值*/

            int j = LocateVex(G, vi);

            if(S[j] = FALSE && D[j] < min)/*找出最小权值的边*/
                {v = vi; min = D[j];}
        }

        S[v-1] = TRUE;                    /*将v加入终点集合*/
        
        int k = LocateVex(G, v);
        
        for(vi = 1; vi <= G.vexnum; vi++) /*更新最短路径和直接前驱数组*/
        {
            int j = LocateVex(G, vi);

            if(S[j] = FALSE && (D[k] + G.arcs[k][j] < D[j])
            {
                D[j] = D[k] + G.arcs[k][j];
                Path[j] = k;
            }
        }
    }
}
                

弗洛伊德算法(顶点对)

求解每一对顶点之间的最短路径有两种方法:

一种是n次调用迪杰斯特拉算法;

另一种是采用下面介绍的弗洛伊德算法。

两种算法的时间复杂度均为O(n³),但弗洛伊德算法的形式更为简洁。

<逻辑思路>

(1)如果说迪杰斯特拉算法是一个“加边”的过程,那么弗洛伊德算法就是一个“加点”的过程;

(2)先确定两个目标顶点,源点为vs,终点为ve;

(3)若vs和ve之间存在弧,初始化为arcs[vs-1][ve-1](编号),反之置为∞;

(4)依次判断剩余顶点vi,若在两个顶点间插入vi后,使得<vs,vi>+<vi,ve>小于<vs,ve>,则置换为vs和ve间的最短路径;

(5)直至所有剩余顶点判断完毕,最短路径确定;

(6)其余的每一对顶点都重复上述过程。

<实现思路>——以邻接矩阵为存储结构的有向网

(1)该算法的核心思想是设置了两个二维辅助数组:

1. 二维数组Path[][]:行表示顶点vi的直接前驱,列表示顶点vi;

2. 二维数组D[]][]:记录vs和ve间的最短路径。

(2)弗洛伊德算法的代码思路比较清晰,插入、比较、更新。

void ShortestPath_Floyd(AMGraph G)
{
/*-----------初始化------------*/
    for(int vs = 1; vs <= G.vexnum; vs++)         /*源点vs*/
    {
        int i = LocateVex(G, vs);

        for(int ve = 1; ve <= G.vexnum; ve++)     /*终点ve*/
        {
            int j = LocateVex(G, ve);

            D[i][j] = G.arcs[i][j];
            
            if(D[i][j] < MaxInt && i != j)
                Path[i][j] = i;
            else Path[i][j] = -1;
        }
           
/*---------初始化结束--------*/

    for(int vi = 1; vi <= G.vexnum; vi++)         /*插入点vi*/
        for(int vs = 1; vs <= G.vexnum; vs++)     /*源点vs*/
            for(int ve = 1; ve <= G.vexnum; ve++) /*终点ve*/
                {
                    int k = LocateVex(G, vi);
                    int i = LocateVex(G, vs);
                    int j = LocateVex(G, ve);
            
                    if(D[i][k] + D[k][j] < D[i][j])       /*更新*/
                    {
                        D[i][j] = D[i][k] + D[k][j];
                        
                        Path[i][j] = k;
                    }
                }
}

路过的圈毛君:“‘拓扑排序’和‘关键路径’的内容可能要先放一放,以后一定会补上的!_(:з」∠)_”

猜你喜欢

转载自blog.csdn.net/Ha1f_Awake/article/details/84618233