简介
关键路径是指工程项目从开始到结束经过的耗时最长的逻辑路径,因此优化关键路径是一种提高工程项目有效方法。
关键路径基于拓扑排序,且引出了以下四个概念。
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; //随便给一个就行了,能表示是那条边就行了
}
}
}