数据结构与算法-Day15-拓扑排序/关键路径

451 阅读8分钟

拓扑排序

AOV网

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

拓扑序列

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

拓扑排序

拓扑排序就是一个有向无环图构造拓扑序列的过程。在构建拓扑排序的过程中:

  1. 如果此图中全部的顶点都被输出,则说明它是不存在环的AOV网
  2. 如果输出的顶点不包含全部顶点,则说明这个图存在环,不是AOV网

因为在有向图中有可能存在没有路径的点,因此一个有向无环图的拓扑序列不是唯一的。

例如上图中:
C1->C2->C3->c4->C5->C6->C7->C8属于一个拓扑序列
C1->C2->C5->c3->C4->C6->C7->C8也属于一个拓扑序列

思路

  1. AOV网中选择一个入度为0的顶点输出
  2. 然后删去此顶点,并删除该顶点发出的全部有向边。
  3. 继续重复此步骤,直到输出全部顶点或者AOV网中不存在入度为0的顶点为止。

以上图为例,模拟一下执行流程。

删除C1以及C1发出的三条边。

此时可以删除C2、C3、C4任意一个顶点,我们选择删除C2以及C2发出的一条边。

删除C3以及C3发出的一条边。

删除C4以及C4发出的一条边。

删除C6以及C6发出的一条边。

删除C5以及C5发出的一条边。

删除C7以及C7发出的一条边。

最后删除C8

代码

我们采用邻接表的方式来存储图

/* 邻接表结构****************** */
//边表结点
typedef struct EdgeNode
{
    //邻接点域,存储该顶点对应的下标
    int adjvex;
    //用于存储权值,对于非网图可以不需要
    int weight;
    //链域,指向下一个邻接点
    struct EdgeNode *next;
}EdgeNode;

//顶点表结点
typedef struct VertexNode
{
    //顶点入度
    int in;
    //顶点域,存储顶点信息
    int data;
    //边表头指针
    EdgeNode *firstedge;
}VertexNode, AdjList[MAXVEX];

//图结构
typedef struct
{
    AdjList adjList;
    //图中当前顶点数和边数
    int numVertexes,numEdges;
}graphAdjList,*GraphAdjList;

拓扑排序

Status TopologicalSort_1(GraphAdjList GL){
    int *stack = malloc(sizeof(int)*GL->numVertexes);
    int top = -1;
    
    int i;
    for(i = 0; i < GL->numVertexes; i++) {
        //入度为0的顶点入栈
        if(GL->adjList[i].in == 0) {
            stack[++top] = i;
        }
    }

    int gettop;
    int count = 0;
    EdgeNode *e;
    while(top!=-1) {
        gettop = stack[top--];
        //打印入度为-0的顶点
        printf("%d -> ",GL->adjList[gettop].data);
        //计算count
        count++;
        e = GL->adjList[gettop].firstedge;
        //遍历顶点的连通顶点并删除边,如果连通顶点入度为0,则加入到栈中
        for(; e; e=e->next) {
            if(--(GL->adjList[e->adjvex].in) == 0) {
                stack[++top] = e->adjvex;
            }
        }
    }
    //如果输出顶点个数小于图的顶点个数,则说明有环,不属于AOV网。
    if(count < GL->numVertexes) {
        return ERROR;
    }
    return OK;
}

每一个顶点都会入栈一次,出栈一次,每一条边都会被删掉,因此时间复杂度为O(n+e)n为顶点个数,e为边的个数。

关键路径

拓扑排序的图为AOV网,是一种有向无环图,当我们把AOV图的每一条边加上权值,就变成了AOE网

AOE网

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

  • 没有入边的顶点称为始点或者源点
  • 没有出边的顶点称为终点或者汇点
  • 一般来说一个工程只有一个开始,一个结束,因此一个AOE网只有一个始点,一个源点

关键路径

从源点到汇点具有最大长度的路径叫做关键路径,在关键路径上的活动叫关键活动。

把上图中分别看做是工厂生产线中有特定顺序的操作步骤,他们之间的顺序按照拓扑序列来排列,其中每个弧的权值看做是每个步骤需要执行的时间。那么关键路径的长度就是最快可以完成生产的时间。如果单位为小时的话,上图中最快需要完成生产的时间为1+5+5+6=17小时。

思路

我们可以发现,在关键路径上的步骤必须马不停蹄的执行,如果关键路径上的步骤有了延迟,那么必定会影响这个生产的工期。

接下来需要引出四个概念:

  1. etv:Earliest Time of Vertex事件最早发生的时间,也就是顶点的最早发生时间

  2. ltv:Latest Time of Vertex事件最晚发生的时间,也就是每个顶点对应的事件最晚发生的时间,如果超出时间将会延误整个工期

  3. ete:Earliest Time of Edge活动最早开工的时间,就是弧的最早发生时间

  4. lte:Latest Time of Edge活动最晚发生时间,就是不推迟工期的最晚开工时间

etv

对于关键路径上的某一活动(Vi,Vj)以及它的下一活动(Vj,Vk)来说,因为关键路径上的每一个操作都不能停歇。因此(Vi,Vj)的最早发生时间etv[i](Vj,Vk)的最晚发生时间lte[j]存在如下关系:

etv[i] = w(Vi,Vj)+lte[j]

这样才能确保(Vi,Vj)没有空余时间,持续进行。

接下来就是如何求解etvltv的过程了。

etv的值我们可以在拓扑排序的过程中进行求解,拿顶点C5来说,它必须等活动(C2,C5)(C3,C5)都执行完才能开始。如果只有(C2,C5)执行完很明显需要等待。

由此可以推导出etv的求解公式:

k = 0时
etv[k] = 0;
k != 0时
etv[k] = max(etv[i]+w(i,k)) 其中i为指向k的顶点,w(i,k)为边的权重

求解etv的代码和拓扑排序的代码类似,因为拓扑排序会按顺序遍历所有的边,我们在遍历边的过程中就可以进行etv的求解。同时将拓扑排序的结果按逆序存储在一个新栈stack2中,以供后续使用。

核心代码如下

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

//进行一遍拓扑排序,求解etv
Status TopologicalSort_1(GraphAdjList GL){
    int *stack = malloc(sizeof(int)*GL->numVertexes);
    int top = -1;
    
    int i;
    for(i = 0; i < GL->numVertexes; i++) {
        //入度为0的顶点入栈
        if(GL->adjList[i].in == 0) {
            stack[++top] = i;
        }
    }
    int gettop;
    int count = 0;
    EdgeNode *e;
    while(top!=-1) {
        gettop = stack[top--];
        //计算count
        count++;
        //栈顶元素入栈stack2
        stack2[++top2] = gettop;
        e = GL->adjList[gettop].firstedge;
        //遍历顶点的连通顶点并删除边,如果连通顶点入度为0,则加入到栈中
        for(; e; e=e->next) {
            if(--(GL->adjList[e->adjvex].in) == 0) {
                stack[++top] = e->adjvex;
            }
            //计算栈顶顶点一遍元素的各个邻接点的etv值,因为拓扑排序的特性,栈顶的顶点一定是没有入度的顶点。
            if(etv[gettop]+e->weight > etv[e->adjvex]) {
                etv[e->adjvex] = etv[gettop]+e->weight;
            }
        }
    }
    //如果输出顶点个数小于图的顶点个数,则说明有环,不属于AOV网。
    if(count < GL->numVertexes) {
        return ERROR;
    }
    return OK;
}

ltv事件最晚发生的时间

这时候需要用我们在上一步保留的stack2。因为stack2中保存的是拓扑序列的逆序,可以确保以栈顶顶点为起点的边的另一顶点肯定已经弹出,就是说ltv值已经求解,通过这一点我们可以来计算ltv

通过etv的求解我们可以得到C8的最早发生时间为17。初始化ltv数组的值为17

对于任意顶点k,它的ltv值存在如下公式

k = 8时
ltv[k] = 17;
k < 8时
ltv[k] = min(ltv[k], ltv[i] - w(k,i)),其中i为以k为出发点的边的另一个顶点。

计算出etvltv之后,再来看etelte。对于边(i,j)来说,最早发生时间和i的最早发生时间etv[i]是一致的,最晚发生时间和j的最晚发生时间ltv[j]的关系为etv[i] = ltv[j]-w(i,j)。这个条件可以来判断一条边是否为关键路径。
代码

void CriticalPath_1(GraphAdjList GL) {
    TopologicalSort_1(GL);
    //打印etv数组(事件最早发生时间)
    printf("etv:\n");
    int i;
    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[i] = etv[GL->numVertexes-1];
    }
    int gettop;
    EdgeNode *e;
    while(top2 != 0) {
        gettop = stack2[top2--];
        e = GL->adjList[gettop].firstedge;
        for(; e; e = e->next) {
            //求解ltv
            if(ltv[gettop] > (ltv[e->adjvex] - e->weight)) {
                ltv[gettop] = ltv[e->adjvex] - e->weight;
            }
        }
    }
    
    //打印ltv 数组
    printf("ltv:\n");
    for (i = 0 ; i < GL->numVertexes; i++) {
        printf("ltv[%d] = %d \n",i,ltv[i]);
    }
    
    int k,ete,lte;
    for(i = 0; i < GL->numVertexes; i++) {
        e = GL->adjList[i].firstedge;
        for(; e; e = e->next) {
            k = e->adjvex;
            ete = etv[i];
            lte = ltv[k] - e->weight;
            if(ete == lte) {
                printf("<%d-%d> length:%d\n",GL->adjList[i].data, GL->adjList[k].data, e->weight);
            }
        }
    }
}

时间复杂度:

  • etv数组时间复杂度为O(n+e)
  • ltv数组时间复杂度为O(n+e)
  • 判断是否为关键路径复杂度为O(n+e)
  • 时间复杂度为O(n)