数据结构与算法----关键路径

970 阅读5分钟

简介

关键路径是指工程项目从开始到结束经过的耗时最长的逻辑路径,因此优化关键路径是一种提高工程项目有效方法。

关键路径基于拓扑排序,且引出了以下四个概念。

Ve(j):表示事件(顶点)的最早开始时间,在不推迟整个工期的前提下,表示从源点开始到该节点的需要的最长时间

Vl(j):表示事件(顶点)的最晚开始时间,在不推迟整个工期的前提下,表示从结束顶点到该点最短需要多少时间

e(i):表示活动(边)的最早开始时间,就是活动边的起点的最早发生时间, 表示该边起点的最早开始时间

l(i):表示活动(边)的最晚开始时间,就是该活动边起点的最晚发生时间,表示该边起点的最晚开始事件

这四个概念后面分别用顶点最早开始时间、顶点最晚开始时间、活动最早开始时间、活动最晚开始时间表示。

获取关键路径

由概念可得,关键路径即耗时最长的路径

再参考下图,因为一个工程由若干个活动按照一定顺序组成,想获取关键路径,那么只需要获取到最长路径(活动)路线即可;

而每个活动的最早与最晚开始时间决定着其耗时,参考其概念(一个从起点处取最长距离,一个从工程结束处取最长距离), 可以推断出当活动最早开始时间与最晚开始时间相等时,那么活动为关键路径

又由概念可得:活动最早开始时间与活动最晚开始时间,可以通过顶点最早开始时间与顶点最晚开始时间获取

因此只需要先获取:顶点最早开始时间、顶点最晚开始时间

顶点最早开始时间

其获取逻辑为:在不推迟整个工期的前提下,表示从源点开始到该节点的需要的最长时间

因此经过如下步骤可以获取:

1.经过拓扑排序,默认每个顶点最早开始时间为0

2.拓扑排序过程有个减少入度的过程,当入度减少时,对比:当前活动起点(当前顶点)的最早开始时间 + 此活动执行时间 > 当前活动终点(下一个顶点)最早开始时间, 如果满足条件,说明该前置活动所在路径耗时更长,因此需要更新时间为更晚

参考关键路径连通图,以V5为例,第一次拓扑为V3-V5时,此时更新V5的最早开始时间应为a2+a5=5;继续拓扑,当执行到V2-V5时,此时更新V5的最早开始时间应为a1+a4=7,大于先前的时间,则更新为最大时间7

3.获取到拓扑排序结果

顶点最晚开始时间

其获取逻辑为:在不推迟整个工期的前提下,表示从结束顶点到该点最短需要多少时间

由于先前获取顶点最早开始时间的过程,经过了拓扑排序,获取到了拓扑的结果(V1-V4-V6-V3-V2-V5-V8-V7-V9),可以利用拓扑结果来获取顶点最晚开始时间,可以参考关键路径连通图

经过如下步骤获取:

1.倒序遍历拓扑排序结果,默认每个顶点最晚开始时间为最大工期18

2.获取拓扑索引对应的出边索引,对比:当前活动终点(下一个顶点)的最晚开始时间 - 此活动执行时间 < 当前活动起点(当前顶点)最晚开始时间, 如果条件满足,即当前顶点更新为更小的那个,说明此顶点后面必须用到的时长更长,需要更新,因此时间上要更早

参考关键路径连通图,以V5为例,第一次遍历为V5-V7时,此时更新V5的最晚开始时间应为最大工时 18-a10-a7=7;当执行到V5-V8时,此时更新V5的最晚开始时间应为18-a11-a8=7,不满足则不需要更新

活动最早开始时间、最晚开始时间

别获取其活动起点顶点的最早开始时间和最晚开始时间即可

关键路径

将活动最早开始时间与活动最晚开始时间进行对比,如果相同,则加入关键路径集合即可

图中标记绿色的即为关键路径图

代码实现以及逻辑

代码实现逻辑如下:

1.声明拓扑排序结果、顶点最早开始时间、顶点最晚开始时间数组,长度为顶点个数N,且最早、最晚开始时间数组分别赋值为0、最长工期

2.声明活动最早开始时间、活动最晚开始时间数组,长度为所有边的个数(通过遍历邻接表,各个顶点的出边即可获取)

3.进行拓扑排序,获取拓扑排序结果,在拓扑排序结果内获取最早开始时间,如果当前顶点的最早开始时间 + 当前活动时间 > 下一个顶点的最早开始时间,则更新为最大的那个,跳到步骤5

4.如果没有在拓扑排序中获取最早开始时间,则正序遍历拓扑排序结果,更新最早开始时间逻辑,同步骤3

5.倒序遍历拓扑排序结果,获取顶点最晚开始时间, 如果当前活动终点(下一个顶点)的最晚开始时间 - 此活动执行时间 < 当前活动起点(当前顶点)最晚开始时间,满足更新为最小的那个即为最晚开始时间

6.通过顶点最早开始时间和顶点最晚开始时间,给予活动最早开始时间与活动最晚开始时间赋值

7.检测活动最早开始时间和活动最晚开始时间一致,放置到最短路径数组中即可

#define N 9 //总顶点数
#define MAXTIME 18 //最长工期

//出度linkList基础结构
typedef struct AdjvexNode {
    int adjvex; //顶点
    int weight; //代表边的权值(即单个活动时长)
    struct AdjvexNode *next; //这个顶点是没有顺序的,单纯是一个顶点所指向的节点而已,表示的就是一条边
} LSAdjvexNode;

typedef struct {
    int vertex; //顶点
    int id; //入度
    LSAdjvexNode *link;
}LSVertexNode;

//拓扑排序,顺道获取顶点的最早开始时间和排序后的结果
void tuppoSort(LSVertexNode *list, int *fastStartList, int *tupoResultList) {
    //巧妙利用入度为0的顶点作为栈的一部分,设置top指向-1即栈底,入度为0的id对齐进行入栈,top指向他,其被加入栈底
    int top = -1;
    for (int i = 0; i < N; i++) {
        if (!list[i].id) {
            list[i].id = top;
            top = i;
        }
    }
    
    int num = 0; //num表示的是拿出了几个,如果 num<N 表示里面有环
    while (top != -1) {
        num++; //默认找到了,数量+1;
        printf("%d ", top + 1);
        tupoResultList[num] = top; //保存索引结果到数组中
        
        LSAdjvexNode *link = list[top].link; //获取出边link
        
        int lineIdx = list[top].vertex; //当前边的起点索引
        
        top = list[top].id; //及时更新为下一个的,因为其后续link节点可能还会更新top
        
        while (link) {
            int idx = link->adjvex; //这个adjvex指向的不是索引,比索引要大一个,即当前边的终点实际索引
            
            //如果此边的上一个 起点最早开始时间 + 事件消耗时间 > 此边终点的最早开始时间,那么更新(判断条件主要是更新一个顶点多个事件的情况,取最长的)
            if (fastStartList[lineIdx] + link->weight > fastStartList[idx]) {
                fastStartList[idx] = fastStartList[lineIdx] + link->weight;
            }
            
            list[idx].id--;
            if (!list[idx].id) {
                //入度为0,可以作为下次待遍历的点
                list[idx].id = top;
                top = idx; //跟一开始一样做标记,跟top值交换
            }
            link = link->next;
        }
    }
    if (num < N) {
        printf("\n中间有回路!");
    }else {
        printf("\n");
    }
    
    //注意结果可能和你想想的不太一样,只要入度为零的谁在前后都一样哈
}

//用于保存边集合,用于确定关键路径涉及到的边
typedef struct {
    int fromvex;
    int tovex;
    int weight;
} LSLinesNode;

int getLinkListLinesCount(LSVertexNode *list) {
    int count = 0;
    for (int i = 0; i < N; i++) {
        LSAdjvexNode *link = list[i].link;
        while (link) {
            link = link->next;
            count++;
        }
    }
    return count;
}

//分别获取连通图的 顶点的最早开始时间,最晚开始时间, 最早开始活动,最晚开始活动
void showCriticalPath(SVertexNode *list) {
    int linesCount = getLinkListLinesCount(list); //获取总边数,用于初始事件(边)
    //分别是拓扑排序的结果,顶点最早开始时间,顶点最晚开始时间
    int tupoResultList[N], fastStartList[N], slowEndList[N];
    //活动(边)最早开始时间,活动(边)最晚开始时间
    LSLinesNode fastStartActivityList[linesCount], slowEndActivityList[linesCount];
    
    //初始化事件(边)最早开始时间和最晚开始时间默认值
    for (int i = 0; i < N; i++) {
        fastStartList[i] = 0;
        slowEndList[i] = MAXTIME;
    }
    
    //进行一波拓扑排序,获得结果的过程顺道获取到最早开始时间,通过拓扑排序结果倒序算出最晚开始时间
    tuppoSort(list, fastStartList, tupoResultList);
    
    //用于获取顶点的最晚开始时间
    for (int i = N - 1; i >= 0; i--) {
        int idx = tupoResultList[i];
        LSAdjvexNode *link = list[idx].link;
        while (link) {
            int j = link->adjvex;
            //反向获取最晚开始时间,最晚开始时间终点 - 事件长度 < 最晚开始时间起点 (最晚开始时间倒着取一定要取最小的,得保证后面的最长任务能完成)
            if (slowEndList[j] - link->weight < slowEndList[idx]) {
                slowEndList[idx] = slowEndList[j] - link->weight;
            }
        }
    }
    
    //而边的的最早开始时间和最晚开始时间,分别对应该边的起点的最早开始时间,和起点的最晚开始时间
    int nodeNum = 0;
    for (int i = 0; i < N; i++) {
        LSAdjvexNode *link = list[i].link; //起点
        while (link) {
            int  j = link->adjvex - 1; //终点
            //初始化最早开始时间的边
            LSLinesNode node = fastStartActivityList[nodeNum];
            node.fromvex = i;
            node.tovex = j;
            node.weight = fastStartList[i];
            
            //初始化最晚开始时间的边
            LSLinesNode node2 = slowEndActivityList[nodeNum];
            node2.fromvex = i;
            node2.tovex = j;
            node2.weight = slowEndList[i];
            link = link->next;
            nodeNum++;
        }
    }
    
    //遍历活动最早时间和最晚时间,对比 最早开始时间 == 最晚开始时间的即为关键路径
    LSLinesNode keyPath[linesCount]; //由边组成的关键路径
    int keyPathCount = 0; //关键路径的实际数量
    //求出关键路径
    for (int i = 0; i < linesCount; i++) {
        LSLinesNode node = fastStartActivityList[i];
        LSLinesNode node2 = slowEndActivityList[i];
        if (node.weight == node2.weight) {
            keyPath[keyPathCount++] = node; //随便给一个就行了,能表示是那条边就行了
        }
    }
}