数据结构与算法之拓扑排序以及关键路径

477 阅读8分钟

拓扑排序

什么是拓扑排序

  • AOV网:在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系,这样有向图的顶点表示活动的网,这个网叫AOV网,如下图所示


  • 拓扑序列:设G=(V,E)是一个具有n个顶点的有向图,V中的顶点序列为V1、V2 .....Vn,若满足从顶点Vi到Vj有一条路径,且Vi在Vj之前,则这样的顶点序列为拓扑序列。

  • 拓扑排序求拓扑序列的过程即为拓扑排序需要注意的是拓扑排序的结果不是唯一,如上图中所示,结果可以是C1->C2->C3->C4->C5,也可以是C2->C1->C3->C4->C5。

  • 拓扑排序的两种结果

  1. 此网中所有的顶点被输出,则说明它是不存在环的AOV网
  2. 如果顶点少了,说明网中存在环,不是AOV网

拓扑排序存储

  • 存储使用邻接表,邻接表结构如图

    其中in表示入度,data表示顶点信息,firstedge表示边表头指针,指向下一个顶点

  • 存储结构示意图


拓扑排序代码实现

  • 算法基本思路:从AOV网中选择一个入度为0的顶点开始输出,然后删除该顶点,并且删除以该顶点为尾的弧;继续重复此步骤直到输出全部顶点或AOV网中的不存在入度为0的顶点
  • 核心代码实现

    void CreateALGraph(MGraph G,GraphAdjList *GL)
    {
        int i,j;
        EdgeNode *e;
        
        //创建图
        *GL = (GraphAdjList)malloc(sizeof(graphAdjList));
        //对图中的顶点数.弧数赋值
        (*GL)->numVertexes=G.numVertexes;
        (*GL)->numEdges=G.numEdges;
        
        //读入顶点信息,建立顶点表
        for(i= 0;i <G.numVertexes;i++)
        {
            (*GL)->adjList[i].in=0;
            (*GL)->adjList[i].data=G.vexs[i];
            //将边表置为空表
            (*GL)->adjList[i].firstedge=NULL;
        }
        
        //建立边表
        for(i=0;i<G.numVertexes;i++)
        {
            for(j=0;j<G.numVertexes;j++)
            {
                if (G.arc[i][j]==1)
                {
                    //创建空的边表结点
                    e=(EdgeNode *)malloc(sizeof(EdgeNode));
                    //邻接序号为j
                    e->adjvex=j;
                    // 将当前顶点上的指向的结点指针赋值给e
                    e->next=(*GL)->adjList[i].firstedge;
                    //将当前顶点的指针指向e
                    (*GL)->adjList[i].firstedge=e;
                    (*GL)->adjList[j].in++;
                    
                }
            }
        }
    }
    
    #pragma mark - 拓扑排序
    /**
     思路:
        1、使用栈处理拓扑排序
        2、循环链接表找到入度为0的顶点,将该顶点入栈
        3、循环栈结构出栈,并打印出栈的顶点,且记录出栈顶点的个数
        4、出栈后,循环遍历与出栈顶点相连接的弧,若与出栈顶点相连接的弧入度不为0则入栈
        5、循环栈结构结束后判断记录出栈顶点的个数与AOV网中顶点个数是否一致,若一致则是AOV网,否则不是
     复杂度:时间复杂度O(n + e),其中n为顶点的个数,e为与出栈顶点相连接弧的最多的个数
     */
    int AOVMap(GraphAdjList Map){
        //栈顶下标
        int top = 0;
        //记录出栈的顶点
        int count = 0;
        //构造栈结构
        int *stack = (int *)malloc(sizeof(int) * Map->numVertexes);
        for (int i = 0; i<Map->numVertexes; i++) {
            if (Map->adjList[i].in == 0) {
                stack[++top] = i;
            }
        }
        //打印入栈的顶点
        printf("top = %d\n",top);
        //记录栈顶元素
        int getTop = 0;
        //用来记录与出栈顶点相连接的弧的顶点
        int k = 0;
        while (top > 0) {
            //出栈
            getTop = stack[top];
            top--;
            //记录顶点个数自增1
            count++;
            //打印出栈顶点
            printf("%d -> ",getTop);
            //遍历与顶点getTop相连接的弧
            for (EdgeNode *eNode = Map->adjList[getTop].firstedge; eNode; eNode = eNode->next) {
                //记录相连接弧的顶点
                k = eNode->adjvex;
                //如果相连接顶点的入度大于0,则自减1
                if (Map->adjList[k].in >0) {
                    Map->adjList[k].in--;
                }
                if (Map->adjList[k].in == 0) {//如果相连接顶点的入度等于0则入栈
                    stack[++top] = k;
                }
            }
        }
        printf("\n");
        if (count == Map->numVertexes) {//如果最终记录的出栈顶点个数跟图的顶点个数相同则表示是AOV网
            return 1;
        }
        return 0;
    }
    
    
    int main(int argc, const char * argv[]) {
        // insert code here...
        printf("Hello, World!\n");
        MGraph M;
        GraphAdjList Map;
        int result;
        CreateMGraph(&M);
        CreateALGraph(M,&Map);
        result = AOVMap(Map);
        printf("result(1是AVO网,0不是AOV网):%d\n",result);
        return 0;
    }
    
    
    


关键路径

关键路径的定义

  • AOE网:在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动持续的时间,这种用有向图的边表表示活动的网,我们叫做AOE网,如图


  • 源点、汇点:没有入边的顶点叫做源点,如上图中的V0顶点;没有出边的顶点叫终点或汇点

  • 路径长度:路径上各个活动持续时间之和叫路径长度

  • 关键路径:从源点到汇点具有最大的路径叫做关键路径

  • 关键活动:关键路径上的活动叫关键活动

关键路径求解的关键参数

  • 事件最早发生时间etv:即顶点Vk最早发生时间
  • 事件最晚发生时间ltv:即顶点Vk最晚发生时间,如果低于这个时间开始则会导致整个工期延期的时间
  • 活动最早开工时间ete:即弧Ak最早发生时间
  • 活动最晚开工时间lte:即弧Ak最晚发生时间,也就是不推迟延期的最晚开工时间

关键路径存储结构

关键路径的存储结构使用AOV网的存储结构

关键路径的求解过程

  • 最早开始时间etv:就是求解AOV网的过程,需要记录每个顶点的最早开工时间,当有多个权值时取最大的那个,如图所示


  • 最晚开始时间ltv:求解ltv要从汇点(终点)开始计算,因为终点不延期其他的就不会延期。ltv求解公式为


  • ete、lte求解:求解图示


  • 关键路径核心代码实现

    //存储拓扑序列的栈
    int *stack2;
    //指向stack2的栈顶下标
    int top2;
    //最早开工以及最晚开工数组
    int *etv,*ltv;
    #pragma mark - 进行拓扑排序
    int AOVSort(GraphAdjList G) {
        //栈顶下标
        int top = 0;
        //栈顶元素
        int getTop;
        //出栈顶点个数
        int count = 0;
        //构造栈
        int *stack = (int *)malloc(sizeof(int) * G->numVertexes);
        //遍历链接表,将入度为0的顶点入栈
        for (int i = 0; i<G->numVertexes; i++) {
            if (G->adjList[i].in == 0) {
                //注意此处入栈的是顶点在数组中的下标,不是顶点本身
                stack[++top] = i;
            }
        }
        //初始化top2
        top2 = 0;
        //初始化AOV序列栈
        stack2 = (int *)malloc(sizeof(int) * G->numVertexes);
        //初始化etv
        etv = (int *)malloc(sizeof(int) * G->numVertexes);
        for (int i = 0; i<G->numVertexes; i++) {
            //初始化最早开始时间为0
            etv[i] = 0;
        }
        //定义关联的顶点下标
        int k;
        printf("拓扑排序:\n");
        while (top>0) {
            //出栈并获得栈顶元素(顶点在数组中的下标)
            getTop = stack[top--];
            //出栈计数自增1
            count++;
            //打印出栈元素
            printf("%d -> ",G->adjList[getTop].data);
            //将出栈的元素入栈到拓扑序列栈中
            stack2[++top2] = getTop;
            //遍历链接表,查找与出栈相链接的顶点
            for (EdgeNode *eNode = G->adjList[getTop].firstedge; eNode; eNode = eNode->next) {
                //获得下标
                k = eNode->adjvex;
                if (G->adjList[k].in>0) {//相链接的顶点入度大于0则自减1
                    G->adjList[k].in--;
                }
                if (G->adjList[k].in == 0) {//自减1后如果等于0则入栈
                    stack[++top] = k;
                }
                //求相连接顶点最早开工时间
                if (etv[k] < etv[getTop] + eNode->weight) {
                    etv[k] = etv[getTop] + eNode->weight;
                }
            }
        }
        printf("\n");
        printf("打印最早发生数组\n");
        for (int i = 0; i<G->numVertexes; i++) {
            printf("etv[%d] = %d\n",i,etv[i]);
        }
        if (count == G->numVertexes) {
            return 1;
        }
        return 0;
    }
    #pragma mark - 求解关键路径
    void AOEKeyPath(GraphAdjList G){
        //进行拓扑排序并获得最早开始时间数组etv
        AOVSort(G);
        //申明最早开工时间与最晚开工时间ete、lte
        int ete,lte;
        //初始化最晚开始时间ltv
        ltv = (int*)malloc(sizeof(int) * G->numVertexes);
        for (int i = 0; i<G->numVertexes; i++) {
            //初始化ltv的最晚开始时间跟etv的最后一个顶点的开始时间一样
            ltv[i] = etv[G->numVertexes - 1];
        }
        //定义栈顶元素
        int getTop;
        //定义与栈顶元素getTop相连接的顶点下标k
        int k;
        //遍历拓扑序列栈,获得ltv
        while (top2 > 0) {
            getTop = stack2[top2--];
            //遍历与下标是getTop的顶点相连接的顶点
            for (EdgeNode *eNode = G->adjList[getTop].firstedge; eNode; eNode = eNode->next) {
                //获得相连接的顶点下标
                k = eNode->adjvex;
                //取较小的值
                if (ltv[getTop] > ltv[k] - eNode->weight) {
                    ltv[getTop] = ltv[k] - eNode->weight;
                }
            }
        }
        //打印ltv数组
        printf("打印ltv数组\n");
        for (int i = 0; i<G->numVertexes; i++) {
            printf("ltv[%d] = %d\n",i,ltv[i]);
        }
        //求解ete、lte,如果两者相等,则该顶点为关键路径上的顶点
        for (int i = 0; i<G->numVertexes; i++) {
            for (EdgeNode *eNode = G->adjList[i].firstedge; eNode; eNode = eNode->next) {
                //获得与i连接的顶点下标
                k = eNode->adjvex;
                //最早开工时间等于最早开始时间
                //ete 就是表示活动 <Vk, Vi> 的最早开工时间, 是针对这条弧来说的.而这条弧的弧尾顶点Vk 的事件发生了, 它才可以发生. 因此ete = etv[k];
                ete = etv[i];
                //最晚开工时间等于最晚开始时间减去耗费时间
                //lte 表示活动<Vk, Vi> 的最晚开工时间, 但此活动再晚也不能等Vi 事件发生才开始,而是必须在Vi事件之前发生. 所以lte = ltv[i] - len<Vk, Vi>.
                lte = ltv[k] - eNode->weight;
                if (ete == lte) {
                    printf("<%d-%d> length = %d\n",G->adjList[i].data,G->adjList[k].data,eNode->weight);
                    printf("关键路径顶点:-> %d \n",G->adjList[i].data);
                }
            }
        }
        printf("\n");
    }