前面我们说过的拓扑排序主要是为解决一个工程能否顺序进行的问题,但有时我们还需要解决工程完成需要的最短时间问题。如果我们要对一个流程图获得最短时间,就必须要分析它们的拓扑关系,并且找到当中最关键的流程,这个流程的时间就是最短时间。
在前面讲了AOV网的基础上,来介绍一个新的概念。在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动的持续时间,这种有向图的边表示活动的网,称之为AOE网(Activity On edge Network)。由于一个工程,总有一个开始,一个结束,在正常情况下,AOE网只有一个源点一个汇点。
既然AOE网是表示工程流程的,所以就具有明显的工程属性。只有在某顶点代表的事件发生后,从该顶点出发的各活动才能开始。只有在进入某顶点的各活动都已经结束,该顶点代表的事件才能发生。
尽管AOV网和AOE网都是用来对工程建模的,但它们还是有很大的区别,主要体现在AOV网是顶点表示活动的网,它只描述活动之间的制约关系,而AOE网是用边表示活动的网,边上的权值表示活动持续的时间,如图7-9-3所示两图的对比。因此,AOE网是要建立在活动之间制约关系没有矛盾的基础之上,再来分析完成整个工程需要多少时间,或者为缩短完成工程所需时间,应当加快哪些活动等问题。
我们把路径上各个活动所持续的时间之后称为路径长度,从源点到汇点具有最大长度的路径叫关键路径,在关键路径上完成的活动叫关键活动。显然就图7-9-3的AOE网而言,开始->发动机完成->部件集中到位->组装完成就是关键路径,路径长度为5.5。
如果我们需要缩短整个工期,去改进轮子的生产效率,哪怕改动成0.1也无益于整个工期的变化,只有缩短关键路径上的关键活动时间才才可以减少整个工期长度。例如如果发动机制造缩短为2.5,整车组装缩短为1.5,那么关键路径就为4.5,整整缩短了一天的时间。
如果某项活动的最早开始时间和最晚开始时间一样,表示中间没有空隙,则此项活动就为关键活动。为此,我们需要定义以下几个参数。
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 的最晚发生时间,也就是不推迟工期的最晚开工时间。
我们首先求得1和2,而 ete 本来是表示活动<vk, vj> 的最早开工时间,是针对弧来说的,但只有此弧的弧尾顶点vk的事件发生了,它才可以开始,因此ete = etv[k]。
而lte 表示的是活动<vk, vj> 的最晚开工时间,但此活动再晚也不能等vj 事件发生才开始,所以lte = ltv[j] - len<vk, vj> 。
最终,我们再来判断ete 和 lte 是否相等,相等意味着活动没有任何空闲,是关键路径,否则就不是。
现在来谈谈如何求etv 和 ltv。
假设我们现在已经求得顶点v0对应的 etv[0] = 0,顶点v1对应的etv[1] = 3, 顶点v2对应的etv[2] = 4, 现在我们需要求顶点v3对应的etv[3],其实就是求etv[1] + len<v1, v3> 与 etv[2] + len<v2, v3> 的较大值。显然 3+5 < 4+8, 得到etv[3] = 12, 如图7-9-5所示。
由此我们也可以得出计算顶点vk的最早发生时间即求etv[k]的公式是:
其中P[k] 表示所有到达顶点vk的弧的集合。比如图7-9-5的P[3]就是<v1, v3> 和 <v2, v3> 两条弧。len<vi, vk> 是弧<vi, vk>上的权值。
假如我们现在已经求得v9~ v5 顶点的ltv值,现在要求v4 的ltv 值,由邻接表可得到v4 有两条弧<v4, v6>, <v4, v7>,可以得到
ltv[4] = min(ltv[7] - 4, ltv[6] - 9) = 15,如图7-9-8所示。
可以发现,在计算ltv时,其实是把拓扑序列倒过来进行而已,因此可以得到计算顶点vk最晚发生时间即求ltv[k] 的公式是:
其中S[K]表示所有从顶点vk出发的弧的集合。比如图7-9-8的S[4] 就是<v4, v6>和<v4, v7>两条弧,len<vk, vj> 是弧<vk, vj> 上的权值。
现有一AOE网图如图7-9-4所示,我们使用邻接表存储结构,注意与拓扑排序时邻接表结构不同的地方在于,这里弧表结点增加了weight域,用来存储弧的权值。
求解事件的最早发生时间etv的过程,就是我们从头至尾找拓扑序列的过程,因此在求关键路径之前,需要先调用一次拓扑序列算法的代码来计算etv 和 拓扑序列列表,我们针对前面讲过的AOV网与拓扑排序的程序进行改进,代码如下(参考《大话数据结构》):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | /* 拓扑排序,若GL无回路,则输出拓扑排序序列并返回1,若有回路返回0。 */ bool TopologicalSort(GraphAdjList GL) { EdgeNode *pe; int i, k, gettop; int top = 0;/* 用于栈指针下标 */ int count = 0;/* 用于统计输出顶点的个数 */ /* 建栈将入度为0的顶点入栈 */ int *stack = (int *)malloc(GL->numVertexes * sizeof(int)); for (i = 0; i < GL->numVertexes; i++) if (0 == GL->adjList[i].in) stack[++top] = i;/* 将入度为0的顶点入栈 */ top2 = 0; etv = (int *)malloc(GL->numVertexes * sizeof(int)); for (i = 0; i < GL->numVertexes; i++) etv[i] = 0; /* 初始化 */ stack2 = (int *)malloc(GL->numVertexes * sizeof(int)); cout << "TopologicalSort ..." << endl; while (top != 0) { gettop = stack[top--]; cout << GL->adjList[gettop].data << " -> "; count++; /* 输出i号顶点,并计数 */ stack2[++top2] = gettop; /* 将弹出的顶点序号压入拓扑序列的栈 */ for (pe = GL->adjList[gettop].firstedge; pe; pe = pe->next) { k = pe->adjvex; /* 将i号顶点的邻接点的入度减1,如果减1后为0,则入栈 */ if (!(--GL->adjList[k].in)) stack[++top] = k; /* 求各顶点事件的最早发生时间etv值 */ if ((etv[gettop] + pe->weight) > etv[k]) etv[k] = etv[gettop] + pe->weight; } } cout << endl; if (count < GL->numVertexes) return false; else return true; } |
在程序开始处我们声明了几个全局变量:
int *etv,*ltv; /* 事件最早发生时间和最迟发生时间数组,全局变量 */
int *stack2; /* 用于存储拓扑序列的栈 */
int top2; /* 用于stack2的指针 */
其中stack2用来存储拓扑序列,以便后面求关键路径时使用。
上面的拓扑排序函数中除了增加了第12~19行,29行,38~39行,其他跟前面讲过的AOV网与拓扑排序没什么区别。
第12~19行初始化全局变量etv数组、top2和stack2的过程。第29行就是将本来要输出的拓扑序列压入全局栈stack2中。第38~39行很关键,是求etv数组的每一个元素的值,具体求值办法参见AOE网和关键路径。
下面来看求关键路径的算法代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 | /* 求关键路径,GL为有向网,输出G的各项关键活动 */ void CriticalPath(GraphAdjList GL) { EdgeNode *pe; int i, j, k, gettop; int ete, lte;/* 声明活动最早发生时间和最迟发生时间变量 */ TopologicalSort(GL);/* 求拓扑序列,计算数组etv和stack2的值 */ ltv = (int *)malloc(GL->numVertexes * sizeof(int)); /* 事件最早发生时间数组 */ for (i = 0; i < GL->numVertexes; i++) ltv[i] = etv[GL->numVertexes - 1];/* 初始化 */ cout << "etv : "; for (i = 0; i < GL->numVertexes; i++) cout << etv[i] << ' '; cout << endl; while (top2 != 0)/* 出栈是求ltv */ { gettop = stack2[top2--]; /* 求各顶点事件的最迟发生时间ltv值 */ for (pe = GL->adjList[gettop].firstedge; pe; pe = pe->next) { k = pe->adjvex; if (ltv[k] - pe->weight < ltv[gettop]) ltv[gettop] = ltv[k] - pe->weight; } } cout << "ltv : "; for (i = 0; i < GL->numVertexes; i++) cout << ltv[i] << ' '; cout << endl; /* 求ete,lte和关键活动 */ for (j = 0; j < GL->numVertexes; j++) { for (pe = GL->adjList[j].firstedge; pe; pe = pe->next) { k = pe->adjvex; ete = etv[j];/* 活动最早发生时间 */ lte = ltv[k] - pe->weight;/* 活动最迟发生时间 */ if (ete == lte) /* 两者相等即在关键路径上 */ cout << "<v" << GL->adjList[j].data << " - v" << GL->adjList[k].data << "> length: " << pe->weight << endl; } } } |
函数第7行调用求拓扑序列的函数,执行完毕后,全局数组etv和栈stack2 如图7-9-6所示,top2 = 10,也就是说,对于每个事件的最早发生时间,我们已经计算出来了。
第11~12行初始化全局变量ltv数组,因为etv[9] = 27,所以数组ltv值现在为全27。
第19~29行是计算ltv 数组的循环,具体方法参见AOE网和关键路径。
当程序执行到第36行,etv和ltv数组的值如图7-9-9
比如etv[1] = 3, 而ltv[1] = 7,表示如果单位是天的话,哪怕v1整个事件在第7天才开始,也可以保证整个工程的按期完成,可以提前v1事件开始时间,但最早也得第3天开始。
第36~47行是求另两个变量,活动最早开始时间ete和活动最晚开始时间lte,并对相同下标的它们进行比较。两重循环嵌套是对邻接表的顶点和每个顶点的弧表遍历,具体方法参见AOE网和关键路径,举例来说,如图7-9-10,当j = 0时,当k = 2, ete = lte, 表示
弧<v0, v2> 是关键路径,因此打印;当k = 1, ete != lte, 故弧<v0, v1> 不是关键路径。
j = 1 一直到 j = 9为止,做法是完全相同的,最后输出的结果如下图,最终关键路径如图7-9-11所示。