数据结构图之六(关键路径)
【1】关键路径
在我的经验意识深处,“关键”二字一般都是指临界点。
凡事万物都遵循一个度的问题,那么存在度就会自然有临界点。
关键路径也正是研究这个临界点的问题。
在学习关键路径前,先了解一个AOV网和AOE网的概念:
用顶点表示活动,用弧表示活动间的优先关系的有向图:
称为顶点表示活动的网(Activity On Vertex Network),简称为AOV网。
与AOV网对应的是AOE(Activity On Edge)网即边表示活动的网。
AOE网是一个带权的有向无环图。
网中只有一个入度为零的点(称为源点)和一个出度为零的点(称为汇点)。
其中,顶点表示事件(Event),弧表示活动,权表示活动持续的时间。
通常,AOE网可用来估算工程的完成时间。
假如汽车生产工厂要制造一辆汽车,制造过程的大概事件和活动时间如上图AOE网:
我们把路径上各个活动所持续的时间之和称为路径长度,从源点到汇点具有最大长度的路径叫关键路径,在关键路径上的活动叫关键活动。
那么,显然对上图AOE网而言,所谓关键路径:
开始-->发动机完成-->部件集中到位-->组装完成。路径长度为5.5。
如果我们试图缩短整个工期,去改进轮子的生产效率,哪怕改动0.1也是无益的。
只有缩短关键路径上的关键活动时间才可以减少整个工期的长度。
例如如果制造发动机缩短为2.5天,整车组装缩短为1.5天,那么关键路径为4.5。
工期也就整整缩短了一天时间。
好吧! 那么研究这个关键路径意义何在?
假定上图AOE网中弧的权值单位为小时,而且我们已经知道黑深色的那一条为关键路径。
假定现在上午一点,对于外壳完成事件而言,为了不影响工期:
外壳完成活动最早也就是一点开始动工,最晚在两点必须要开始动工。
最大权值3表示所有活动必须在三小时之后完成,而外壳完成只需要2个小时。
所以,这个中间的空闲时间有一个小时,为了不影响整个工期,它必须最迟两点动工。
那么才可以保证3点时与发动机完成活动同时竣工,为后续的活动做好准备。
对AOE网有待研究的问题是:
(1)完成整个工程至少需要多少时间?
(2)那些活动是影响工程进度的关键?
今天研究是实例如下图所示:
假想是一个有11项活动的AOE网,其中有9个事件(V1,V2,V3...V9)。
每个事件表示在它之前的活动已经完成,在它之后的活动可以开始。
如V1表示整个工程开始,V9表示整个共结束,V5表示a4和a5已经完成,a7和a8可以开始。
【2】关键路径算法
为了更好的理解算法,我们先需要定义如下几个参数:
(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[k]和最晚开工时间lte[k]相等判断ak是否是关键路径。
将AOE网转化为邻接表结构如下图所示:
与拓扑序列邻接表结构不同的地方在于,弧链表增加了weight域,用来存储弧的权值。
求事件的最早发生时间etv的过程,就是从头至尾找拓扑序列的过程。
因此,在求关键路径之前,先要调用一次拓扑序列算法的代码来计算etv和拓扑序列表。
数组etv存储事件最早发生时间
数组ltv存储事件最迟发生时间
全局栈用来保存拓扑序列
注意代码中的粗部分与原拓扑序列的算法区别。
第11-15行 初始化全局变量etv数组。
第21行 就是讲要输出的拓扑序列压入全局栈。
第 27-28 行很关键,它是求etv数组的每一个元素的值。
比如:假如我们已经求得顶点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,在代码中e->weight就是当前弧的长度。如图所示:
由此也可以得到计算顶点Vk即求etv[k]的最早发生时间公式如上。
下面具体分析关键路径算法:
1. 程序开始执行。第5行,声明了etv和lte两个活动最早最晚发生时间变量
2. 第6行,调用求拓扑序列的函数。
执行完毕后,全局数组etv和栈的值如下所示796,也就是说已经确定每个事件的最早发生时间。
3. 第7-9行初始化数组ltv,因为etv[9]=27,所以数组当前每项均为27。
4. 第10-19行为计算ltv的循环。第12行,先将全局栈的栈头出栈,由后进先出得到gettop=9。
但是,根据邻接表中信息,V9没有弧。所以至此退出循环。
5. 再次来到第12行,gettop=8,在第13-18行的循环中,V8的弧表只有一条<V8,V9>
第15行得到k=9,因为ltv[9]-3<ltv[8],所以ltv[8]=ltv[9]-3=24,过程如下图所示:
6. 再次循环,当gettop=7,5,6时,同理可计算出ltv相对应的值为19,25,13。
此时ltv值为:{27,27,27,27,27,13,25,19,24,27}
7. 当gettop=4时,由邻接表信息可得到V4有两条弧<V4,V6>和<V4,V7>。
通过第13-18行的循环,可以得到ltv[4]=min(ltv[7]-4,ltv[6]-9)=min(19-4,25-9)=15
过程分析如下图所示:
当程序执行到第20行时,相关变量的值如下图所示。
比如etv[1]=3而ltv[1]=7表示(如果单位按天计的话):
哪怕V1这个事件在第7天才开始也是可以保证整个工程按期完成。
你也可以提前V1时间开始,但是最早也只能在第3天开始。
8. 第20-31行是求另两个变量活动最早开始时间ete和活动最晚时间lte。
当 j=0 时,从V0顶点开始,有<V0,V2>和<V0,V1>两条弧。
当 k=2 时,ete=etv[j]=etv[0]=0
lte=ltv[k]-e->weight=ltv[2]-len<v0,v2>=4-4=0 此时ete == lte
表示弧<v0,v2>是关键活动,因此打印。
当 k=1 时,ete=etv[j]=etv[0]=0
lte=ltv[k]-e->weight=ltv[2]-len<v0,v1>=7-3=4 此时ete != lte
表示弧<v0,v1>并不是关键活动。如图所示:
说明:ete表示活动<Vk,Vj>的最早开工时间,是针对弧来说的。
但是只有此弧的弧尾顶点Vk的事件发生了,它才可以开始,ete=etv[k]。
lte表示的是活动<Vk,Vj>最晚开工时间,但此活动再晚也不能等V1事件发生才开始。
而必须要在V1事件之前发生,所以lte=ltv[j]-len<Vk,Vj>。
9. j=1 直到 j=9 为止,做法完全相同。
最终关键路径如下图所示:
注意:本例是唯一一条关键路径,并不等于不存在多条关键路径。
如果是多条关键路径,则单是提高一条关键路径上的关键活动速度并不是能导致整个工程缩短工期、
而必须提高同时在几条关键路径上的活动的速度。
【3】关键路径是代码实现
本示例代码与算法有些不同,但是效果相同,都是为了达到一个共同目的:理解并学习关键路径算法。
1 #include <iostream>
2 #include "Stack.h"
3 #include <malloc.h>
4 using namespace std;
5
6 #define MAXVEX 10
7 #define MAXEDGE 13
8
9 // 全局栈
10 SeqStack<int> sQ2;
11
12 typedef struct EdgeNode
13 {
14 int adjvex; // 邻接点域,存储该顶点对应的下标
15 int weight; // 边的权值
16 struct EdgeNode* next; // 链域
17 } EdgeNode;
18
19 typedef struct VertexNode
20 {
21 int inNum; // 顶点入度值
22 int data; // 顶点数值欲
23 EdgeNode* firstedge; // 边表头指针
24 } VertexNode, AdjList[MAXVEX];
25
26 typedef struct
27 {
28 AdjList adjList;
29 int numVertexes, numEdges; // 图中当前顶点数和边数(对于本案例,已经存在宏定义)
30 } graphAdjList, *GraphAdjList;
31
32 // 构建节点
33 EdgeNode* BuyNode()
34 {
35 EdgeNode* p = (EdgeNode*)malloc(sizeof(EdgeNode));
36 p->adjvex = -1;
37 p->next = NULL;
38 return p;
39 }
40 // 初始化图
41 void InitGraph(graphAdjList& g)
42 {
43 for (int i = 0; i < MAXVEX; ++i)
44 {
45 g.adjList[i].firstedge = NULL;
46 }
47 }
48 // 创建图
49 void CreateGraph(graphAdjList& g)
50 {
51 int i = 0, begin = 0, end = 0, weight = 0;
52 EdgeNode *pNode = NULL;
53 cout << "输入10个顶点信息(顶点 入度):" << endl;
54 for (i = 0; i < MAXVEX; ++i)
55 {
56 cin >> g.adjList[i].data >> g.adjList[i].inNum;
57 }
58 cout << "输入13条弧的信息(起点 终点 权值):" << endl;
59 for (i = 0; i < MAXEDGE; ++i)
60 {
61 cin >> begin >> end >> weight;
62 pNode = BuyNode();
63 pNode->adjvex = end;
64 pNode->weight = weight;
65 pNode->next = g.adjList[begin].firstedge;
66 g.adjList[begin].firstedge = pNode;
67 }
68 }
69 // 打印输入信息的逻辑图
70 void PrintGraph(graphAdjList &g)
71 {
72 cout << "打印AOE网的邻接表逻辑图:" << endl;
73 for (int i = 0; i < MAXVEX; ++i)
74 {
75 cout << " " << g.adjList[i].inNum << " " << g.adjList[i].data << " ";
76 EdgeNode* p = g.adjList[i].firstedge;
77 cout << "-->";
78 while (p != NULL)
79 {
80 int index = p->adjvex;
81 cout << "[" << g.adjList[index].data << " " << p->weight << "] " ;
82 p = p->next;
83 }
84 cout << endl;
85 }
86 }
87 // 求拓扑序列
88 bool TopologicalSort(graphAdjList g, int* pEtv)
89 {
90 EdgeNode* pNode = NULL;
91 int i = 0, k = 0, gettop = 0;
92 int nCnt = 0;
93 SeqStack<int> sQ1;
94 for (i = 0; i < MAXVEX; ++i)
95 {
96 if (0 == g.adjList[i].inNum)
97 sQ1.Push(i);
98 }
99 for (i = 0; i < MAXVEX; ++i)
100 {
101 pEtv[i] = 0;
102 }
103 while (!sQ1.IsEmpty())
104 {
105 sQ1.Pop(gettop);
106 ++nCnt;
107 sQ2.Push(gettop); // 将弹出的顶点序号压入拓扑序列的栈
108 if (MAXVEX == nCnt)
109 { //去掉拓扑路径后面的-->
110 cout << g.adjList[gettop].data << endl;
111 break;
112 }
113 cout << g.adjList[gettop].data << "-->";
114 pNode = g.adjList[gettop].firstedge;
115 while (pNode != NULL)
116 {
117 k = pNode->adjvex;
118 --g.adjList[k].inNum;
119 if (0 == g.adjList[k].inNum)
120 sQ1.Push(k);
121 if (pEtv[gettop] + pNode->weight > pEtv[k])
122 pEtv[k] = pEtv[gettop] + pNode->weight;
123 pNode = pNode->next;
124 }
125 }
126 return nCnt != MAXVEX;
127 }
128 // 关键路径
129 void CriticalPath(graphAdjList g, int* pEtv, int* pLtv)
130 {
131 // pEtv 事件最早发生时间
132 // PLtv 事件最迟发生时间
133 EdgeNode* pNode = NULL;
134 int i = 0, gettop = 0, k =0, j = 0;
135 int ete = 0, lte = 0; // 声明活动最早发生时间和最迟发生时间变量
136 for (i = 0; i < MAXVEX; ++i)
137 {
138 pLtv[i] = pEtv[MAXVEX-1]; // 初始化
139 }
140 while (!sQ2.IsEmpty())
141 {
142 sQ2.Pop(gettop); // 将拓扑序列出栈,后进先出
143 pNode = g.adjList[gettop].firstedge;
144 while (pNode != NULL)
145 { // 求各顶点事件的最迟发生时间pLtv值
146 k = pNode->adjvex;
147 if (pLtv[k] - pNode->weight < pLtv[gettop])
148 pLtv[gettop] = pLtv[k] - pNode->weight;
149 pNode = pNode->next;
150 }
151 }
152 // 求 ete, lte, 和 关键路径
153 for (j = 0; j < MAXVEX; ++j)
154 {
155 pNode = g.adjList[j].firstedge;
156 while (pNode != NULL)
157 {
158 k = pNode->adjvex;
159 ete = pEtv[j]; // 活动最早发生时间
160 lte = pLtv[k] - pNode->weight; // 活动最迟发生时间
161 if (ete == lte)
162 cout << "<V" << g.adjList[j].data << ",V" << g.adjList[k].data << "> :" << pNode->weight << endl;
163 pNode = pNode->next;
164 }
165 }
166 }
167 void main()
168 {
169 graphAdjList myg;
170 InitGraph(myg);
171 cout << "创建图:" << endl;
172 CreateGraph(myg);
173 cout << "打印图的邻接表逻辑结构:" << endl;
174 PrintGraph(myg);
175
176 int* pEtv = new int[MAXVEX];
177 int* pLtv = new int[MAXVEX];
178
179 cout << "求拓扑序列(全局栈sQ2的值):" << endl;
180 TopologicalSort(myg, pEtv);
181 cout << "打印数组pEtv(各个事件的最早发生时间):" << endl;
182 for(int i = 0; i < MAXVEX; ++i)
183 {
184 cout << pEtv[i] << " ";
185 }
186 cout << endl << "关键路径:" << endl;
187
188 CriticalPath(myg, pEtv, pLtv);
189 cout << endl;
190 }
191 /*
192 创建图:
193 输入10个顶点信息(顶点 入度):
194 0 0
195 1 1
196 2 1
197 3 2
198 4 2
199 5 1
200 6 1
201 7 2
202 8 1
203 9 2
204 输入13条弧的信息(起点 终点 权值):
205 0 1 3
206 0 2 4
207 1 3 5
208 1 4 6
209 2 3 8
210 2 5 7
211 3 4 3
212 4 6 9
213 4 7 4
214 5 7 6
215 6 9 2
216 7 8 5
217 8 9 3
218 打印图的邻接表逻辑结构:
219 打印AOE网的邻接表逻辑图:
220 0 0 -->[2 4] [1 3]
221 1 1 -->[4 6] [3 5]
222 1 2 -->[5 7] [3 8]
223 2 3 -->[4 3]
224 2 4 -->[7 4] [6 9]
225 1 5 -->[7 6]
226 1 6 -->[9 2]
227 2 7 -->[8 5]
228 1 8 -->[9 3]
229 2 9 -->
230 求拓扑序列(全局栈sQ2的值):
231 0-->1-->2-->3-->4-->6-->5-->7-->8-->9
232 打印数组pEtv(各个事件的最早发生时间):
233 0 3 4 12 15 11 24 19 24 27
234 关键路径:
235 <V0,V2> :4
236 <V2,V3> :8
237 <V3,V4> :3
238 <V4,V7> :4
239 <V7,V8> :5
240 <V8,V9> :3
241 */
本示例代码中的Stack.h头文件从随笔《栈》中拷贝即可。
Good Good Study, Day Day Up.
顺序 选择 循环 总结