15.数据结构与算法- 拓扑排序&关键路径

1,049 阅读10分钟

拓扑排序

拓扑排序:解决的是一个工程能否顺序进行的问题

AOV网

在一个表示工程的有向图中,用顶点表示活动,用弧表示活动之间的优先关系。这样的有向图为顶点表示活动的网,我们称为AOV网(Activity On Vertex Network:顶点活动网)

设 G=(V,M)是一个具有 n 个顶点的有向图,V 中的顶点序列 V1,V2...Vn。若满足从顶点 Vi 到 Vj 有一条路径,则顶点序列 Vi 必须在 Vj 之前,则我们称这样的顶点序列成为拓扑序列

所谓拓扑排序,其实就是对一个有向图构造拓扑序列的过程, 构造过程拓扑序列会产生 2 个结果

1. 如果此网中的全部顶点被输出,则说明它不存在环(回路),是 AOV 网
2. 如果输出的顶点数少了,哪怕仅少了ー个,也说明这个网存在环(回路), 不是 AOV 网

算法基本思路

从 AOV 网中选择一个入度为 0 的顶点输出,然后删去此顶点并删除以此顶点为尾的孤。继续重复此步骤,直到输出全部顶点或 AOV 网中不存在入度为 0 的顶点为止

在这个算法实现过程,我们需要借助一个数据结构,来帮助我们解决避免每次查找时,都要去遍历 AOV 图中的顶点表查找有没有入度为 0 的顶点

1. 创建一个栈(stack)用来存储入度 in 为 0 的顶点序号 
2. 遍历 AOV 图中顶点表,判断入度为 0 的顶点全部入栈

关键代码

/*拓扑排序. 若AOV网图无回路则输出拓扑排序的序列并且返回状态值1,若存在回路则返回状态值0*/
/*拓扑排序:解决的是一个工程能否顺序进行的问题!*/
Status TopologicalSort(GraphAdjList GL){
    
    EdgeNode *e;
    int i,k,gettop;
    //用于栈指针下标
    int top=0;
    //用于统计输出顶点的个数
    int count=0;
    
    //建栈将入度为0的顶点入栈(目的:为了避免每次查找时都要遍历顶点表查找有没有入度为0的顶点)
    int *stack=(int *)malloc(GL->numVertexes * sizeof(int) );
    
    //1.遍历邻接表-顶点表,将入度in为0的顶点入栈
    /*参考图1> 此时stack栈中应该成为0,1,3.即V0,V1,V3的顶点入度为0*/
    for(i = 0; i<GL->numVertexes; i++)
        //将入度为0的顶点入栈
        if(0 == GL->adjList[i].in)
            stack[++top]=i;
    printf("top = %d\n",top);
    
    //2.循环栈结构(当栈中有元素则循环继续)
    while(top!=0)
    {
        //出栈
        gettop=stack[top--];
        printf("%d -> ",GL->adjList[gettop].data);
        
        //输出顶点,并计数  用于统计输出的顶点个数 个数与网顶点数相同,则构成AOV网,否则不构成
        count++;
        
        //遍历与栈顶相连接的弧
        for(e = GL->adjList[gettop].firstedge; e; e = e->next)
        {
            //获取与gettop连接的顶点
            k=e->adjvex;
            
            //1.将与gettop连接的顶点入度减1; 目的是为了将Vn顶点的弧删除.
            //2.判断如果当前减1后为0,则入栈
            if( !(-- GL->adjList[k].in) )
                //将k入栈到stack中,并且top加1;
                stack[++top]=k;
        }
    }
    
    /*思考:3 -> 1 -> 2 -> 6 -> 0 -> 4 -> 5 -> 8 -> 7 -> 12 -> 9 -> 10 ->13 -> 11
     这并不是唯一的拓扑排序结果.
     分析算法:将入度为0的顶点入栈的时间复杂度为O(n), 而之后的while 循环,每个顶点进一次栈,并且出一次栈. 入度减1, 则共执行了e次. 那么整个算法的时间复杂度为O(n+e)*/
    
    printf("\n");
    
    //判断是否把所有的顶点都输出. 则表示找到了拓扑排序;
    if(count < GL->numVertexes)
        return ERROR;
    else
        return OK;
}

关键路径

AOE网

在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动的持续时间,这种有向图的边表表示活动的网我们称之为 AOE网(Activity On Edge Network)

AOE网具有以下两个性质:

①只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始。
②只有在进入一某顶点的各有向边所代表的活动都已经结束,该顶点所代表的事件才能发生

没有入边的顶点称为始点或源点,没有出边的顶点称为终点或汇点

AOE网可以看做是带权值的AOV网

由于ー个工程,总有一个开始,一个结束。所以正常情況下,AOE 网只有ー个源点和一个汇点

路径上各个活动所持续的时间之和称为路径长度
从源点到汇点具有最大的路径叫关键路径
在关键路径上的活动叫关键活动

关键路径求解的几个核心参数

  • 1.事件最早发生的时间 etv (earliest time of vertex):顶点 Vk 的最早发生时间(从源点到该点的最长路径代表着该顶点的最早发生时间)
  • 2.事件最晚发生时间 ltv (latest time of vertex):顶点 Vk 的最晚发生时间,也就是每个顶点对应的事件最晚需要开始的时间,超出此时间将会延误整个工期;
  • 3.活动的最早开工时间 ete (earliest time of edge):弧 Ak 的最早发生时间;
  • 4.活动的最晚开工时间 lte (latest time of edge):弧 Ak 的最晚发生时间,也就是不推迟工期的最晚开工时间。
ete 就是表示活动<Vk, Vj>的最早开工时间,是针对这条弧来说的而这条弧的弧尾顶点 k 的事件发生了,它オ可以发生。因此 ete = etv[k];

lte 表示活动<Vk, Vj>的最晚开工时间,但此活动再晚也不能等 Vj 事件发生才开始,而是必须在 Vj事件之前发生。所以 lte = ltv[j] - len<Vk, Vj>

例如,你需要完成 2 小时作业并且在 23 点睡觉。所以你不能在 23 点才开始做作业,而是必须提前 2 小时,在 21 点开始,才有可能完成作

所以,如果判断 ete 与 lte 是否相等,相等就意味着活动之间没有任何空闲时间,是关键活动,否则不是

关键路径法及C语言实现

关键代码

int *etv,*ltv; /* 事件最早发生时间和最迟发生时间数组,全局变量 */
int *stack2;   /* 用于存储拓扑序列的栈 */
int top2;       /* 用于stack2的指针*/

//拓扑排序
Status TopologicalSort(GraphAdjList GL){

    //若GL无回路,则输出拓扑排序序列且返回状态OK, 否则返回状态ERROR;
    EdgeNode *e;
    int i,k,gettop;
    //栈指针下标;
    int top = 0;
    //用于统计输出的顶点个数.作为拓扑排序是否存在回路的判断依据;
    int count = 0;
    //建栈,将入度in = 0的顶点入栈;
    int *stack = (int *)malloc(GL->numVertexes * sizeof(int));

    //遍历顶点表上入度in = 0 入栈
    for (i = 0; i < GL->numVertexes;i++) {
        //printf("%d %d\n",i,GL->adjList[i].in);
        if ( 0 == GL->adjList[i].in ) {
            stack[++top] = i;
        }
    }

    //* stack2 的栈指针下标
    top2 = 0;
    //* 初始化拓扑序列栈
    stack2 = (int *)malloc(sizeof(int) * GL->numVertexes);
    //* 事件最早发生时间数组
    etv = (int *)malloc(sizeof(GL->numVertexes * sizeof(int)));
    //* 初始化etv 数组
    for (i = 0 ; i < GL->numVertexes; i++) {
        //初始化
        etv[i] = 0;
    }

    printf("TopologicSort:\t");
    while (top != 0) {
        
        gettop = stack[top--];
        printf("%d -> ", GL->adjList[gettop].data);
        count++;

        //将弹出的顶点序号压入拓扑排序的栈中;
        stack2[++top2] = gettop;
        
        //例如gettop为V0 ,那么与V0相连接的结点就有etv[1] = 3; etv[2] = 4;
        //例如gettop为V1 ,那么与V1连接的结点就有etv[4]= 3+6=9; etv[3] = 8;
        //例如gettop为V2 ,那么与V2连接的结点就有etv[5]= 4+7=11; etv[3] = 12;
        //例如gettop为V3 ,那么与V3连接的结点就有etv[4]= 12+3=15;
        /// gettop当前顶点下标  k是与当前顶点有关系的顶点下标
        for(e = GL->adjList[gettop].firstedge; e; e = e->next)
        {
            k = e->adjvex;
            
            //将i顶点连接的邻接顶点入度减1,如果入度减一后为0,则入栈
            if(!(--GL->adjList[k].in))
                stack[++top] = k;

            //求各顶点事件的最早发生的时间etv值
            //printf("etv[gettop]+e->weight = %d\n",etv[gettop]+e->weight);
            //printf("etv[%d] = %d\n",k,etv[k]);
            if ((etv[gettop] + e->weight) > etv[k]) {
                etv[k] = etv[gettop] + e->weight;
            }
        }

    }
    printf("\n");
    
    //打印etv(事件最早发生时间数组)
//    for (i = 0; i < GL->numVertexes; i++) {
//        printf("etv[%d] = %d\n",i,etv[i]);
//    }
//    printf("\n");
    
    if(count < GL->numVertexes)
        return ERROR;
    else
        return OK;
    return OK;
}

//求关键路径, GL为有向网,则输出G的各项关键活动;
void CriticalPath(GraphAdjList GL){
    EdgeNode *e;
    int i,gettop,k,j;
    
    //声明活动最早发生时间和最迟发生时间变量;
    int ete,lte;
    
    //求得拓扑序列,计算etv数组以及stack2的值
    TopologicalSort(GL);
   
    //打印etv数组(事件最早发生时间)
    printf("etv:\n");
    for(i = 0; i < GL->numVertexes; i++)
        printf("etv[%d] = %d \n",i,etv[i]);
    printf("\n");
    
    //事件最晚发生时间数组
    ltv = (int *)malloc(sizeof(int) * GL->numVertexes);
   
    //初始化ltv数组
    for (i = 0; i < GL->numVertexes; i++) {
        //初始化ltv数组. 赋值etv最后一个事件的值
        ltv[i] = etv[GL->numVertexes-1];
        //printf("ltv[%d] = %d\n",i,ltv[i]);
    }
    
    //计算ltv(事件最晚发生时间) 出栈求ltv
    while (top2 != 0) {
        
        //出栈(栈顶元素)
        gettop = stack2[top2--];
        
        //找到与栈顶元素连接的顶点; 例如V0是与V1和V2连接
        for (e = GL->adjList[gettop].firstedge; e; e = e->next) {
            //获取与gettop 相连接的顶点
            k = e->adjvex;
            //计算min(ltv[k]-e->weight,ltv[gettop])
            if (ltv[k] - e->weight < ltv[gettop]) {
                //更新ltv 数组
                ltv[gettop] = ltv[k] - e->weight;
            }
        }
    }
    
    //打印ltv 数组
    printf("ltv:\n");
    for (i = 0 ; i < GL->numVertexes; i++) {
        printf("ltv[%d] = %d \n",i,ltv[i]);
    }
    
    printf("\n");
    //求解ete,lte 并且判断lte与ete 是否相等.相等则是关键活动;
    //2层循环(遍历顶点表,边表)
    for(j=0; j<GL->numVertexes;j++)
    {
        for (e = GL->adjList[j].firstedge; e; e = e->next) {
            //获取与j连接的顶点;
            k = e->adjvex;
            //ete 就是表示活动 <Vk, Vj> 的最早开工时间, 是针对这条弧来说的.而这条弧的弧尾顶点Vk 的事件发生了, 它才可以发生. 因此ete = etv[k];
            ete = etv[j];
            //lte 表示活动<Vk, Vj> 的最晚开工时间, 但此活动再晚也不能等Vj 事件发生才开始,而是必须在Vj 事件之前发生. 所以lte = ltv[j] - len<Vk, Vj>.
            lte = ltv[k]-e->weight;
            //如果ete == lte 则输出j,k以及权值;
            if (ete == lte) {
                printf("<%d-%d> length:%d\n",GL->adjList[j].data, GL->adjList[k].data, e->weight);
            }
        }
    }
    
}

/*
TopologicSort:	0 -> 1 -> 2 -> 3 -> 4 -> 6 -> 5 -> 7 -> 8 -> 9 -> 
etv:
etv[0] = 0 
etv[1] = 3 
etv[2] = 4 
etv[3] = 12 
etv[4] = 15 
etv[5] = 11 
etv[6] = 24 
etv[7] = 19 
etv[8] = 24 
etv[9] = 27 

ltv:
ltv[0] = 0 
ltv[1] = 7 
ltv[2] = 4 
ltv[3] = 12 
ltv[4] = 15 
ltv[5] = 13 
ltv[6] = 25 
ltv[7] = 19 
ltv[8] = 24 
ltv[9] = 27 

<0-2> length:4
<2-3> length:8
<3-4> length:3
<4-7> length:4
<7-8> length:5
<8-9> length:3
*/