数据结构之[关键路径]以及[拓扑算法优化]

【1】关键路径(说明部分参考于:https://www.cnblogs.com/Braveliu/p/3461649.html)

在我的经验意识深处,“关键”二字一般都是指临界点。

凡事万物都遵循一个度的问题,那么存在度就会自然有临界点。

关键路径也正是研究这个临界点的问题。

在学习关键路径前,先了解一个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 为止,做法完全相同。

最终关键路径如下图所示:

注意:本例是唯一一条关键路径,并不等于不存在多条关键路径。

如果是多条关键路径,则单是提高一条关键路径上的关键活动速度并不是能导致整个工程缩短工期、

而必须提高同时在几条关键路径上的活动的速度。

代码如下:

  1 #include "stdafx.h"
  2 #include<iostream>
  3 #include<string>
  4 using namespace std;
  5 #define MAXSIZE 100
  6 #define ERROR 0
  7 #define OK 1
  8 #define INFINITY 65535
  9 typedef int Status;
 10 
 11 int *etv, *ltv;                        //事件最早发生时间和最迟发生时间数组,全局变量 
 12 int *stack2;                           //用于存储拓扑序列的栈 
 13 int top2;                               //用于stack2的指针 
 14 
 15 typedef struct ArcNode                 //边表结点
 16 {
 17     int adjvex;                        //改变所指向的顶点的位置
 18     int weight;                        //用于存储权值
 19     struct ArcNode *nextarc;           //指向下一条边的指针    
 20 }ArcNode;
 21 typedef struct VNode                   //顶点表结点
 22 {
 23     int in;
 24     char data;                         //顶点域,存储顶点信息
 25     ArcNode *firstarc;                 //指向第一条依附该顶点的边的指针
 26 }VNode, AdjList[MAXSIZE];              //AdList表示邻接表类型
 27 typedef struct                         //邻接表
 28 {
 29     AdjList vertices;
 30     int vexnum, arcnum;                //图的当前顶点数和边数
 31 }ALGraph;
 32 
 33 typedef struct
 34 {
 35     char vexs[MAXSIZE];                //顶点表
 36     int arcs[MAXSIZE][MAXSIZE];        //邻接矩阵
 37     int vexnum, arcnum;                //图的当前点数和边数
 38 }AMGraph;
 39 
 40 void CreateUDN(AMGraph &G)             //采用邻接矩阵表示法,创建无向网&G
 41 {
 42     int i, j, w;
 43     std::cout << "请输入总顶点数、总边数(空格隔开):" << endl;
 44     cin >> G.vexnum >> G.arcnum;       //输入总顶点数、总边数
 45     std::cout << "请输入顶点信息(空格隔开):" << endl;
 46     for (i = 0; i < G.vexnum; i++)     //依次输入点的信息
 47     {
 48         cin >> G.vexs[i];
 49     }
 50     for (i = 0; i < G.vexnum; i++)     //初始化邻接矩阵,编的权值均为极大值MaxInt
 51         for (j = 0; j < G.vexnum; j++)
 52         {
 53             if (i == j)
 54                 G.arcs[i][j] = 0;
 55             else
 56                 G.arcs[i][j] = INFINITY;
 57         }
 58     std::cout << "请输入边的信息(输入顺序:连接点1编号、连接点2编号以及权值):" << endl;
 59     for (int k = 0; k < G.arcnum; k++) //构造邻接矩阵
 60     {
 61         cin >> i >> j >> w;            //输入一条边依附的顶点
 62         G.arcs[i - 1][j - 1] = w;
 63     }
 64 
 65 }
 66 
 67 void CreateALGraph(AMGraph G, ALGraph &GL)
 68 {
 69     int i, j;
 70     ArcNode *e;
 71     GL.vexnum = G.vexnum;
 72     GL.arcnum = G.arcnum;
 73     for (i = 0; i <G.vexnum; i++)      //读入顶点信息,建立顶点表
 74     {
 75         GL.vertices[i].in = 0;
 76         GL.vertices[i].data = G.vexs[i];
 77         GL.vertices[i].firstarc = NULL;//将边表置为空表
 78     }
 79 
 80     for (i = 0; i<G.vexnum; i++)       //建立边表
 81     {
 82         for (j = 0; j<G.vexnum; j++)
 83         {
 84             if (G.arcs[i][j] != 0 && G.arcs[i][j]<INFINITY)
 85             {
 86                 e = new ArcNode;
 87                 e->adjvex = j;           //邻接序号为j
 88                 e->weight = G.arcs[i][j];
 89                 e->nextarc = GL.vertices[i].firstarc; //将当前顶点上的指向的结点指针赋值给e
 90                 GL.vertices[i].firstarc = e;          //将当前顶点的指针指向e
 91                 GL.vertices[j].in++;
 92             }
 93         }
 94     }
 95 }
 96 
 97 //拓扑排序
 98 Status TopologicalSort(ALGraph GL)
 99 {    //若GL无回路,则输出拓扑排序序列并返回1,若有回路返回0
100     ArcNode *e;
101     int i, k, gettop;
102     int top = 0;                       //用于栈指针下标 
103     int count = 0;                     //用于统计输出顶点的个数
104     int *stack;    /* 建栈将入度为0的顶点入栈  */
105     stack = new int[GL.vexnum];
106     for (i = 0; i<GL.vexnum; i++)
107         if (0 == GL.vertices[i].in)    //将入度为0的顶点入栈
108             stack[++top] = i;
109 
110     top2 = 0;
111     etv = new int[GL.vexnum];          //事件最早发生时间数组
112     for (i = 0; i<GL.vexnum; i++)
113         etv[i] = 0;                    //初始化
114     stack2 = new int[GL.vexnum];       //初始化拓扑序列栈
115 
116     std::cout << "TopologicalSort: ";
117     while (top != 0)
118     {
119         gettop = stack[top--];
120         std::cout << GL.vertices[gettop].data << " -> ";
121         count++;                       //输出i号顶点,并计数
122 
123         stack2[++top2] = gettop;       //将弹出的顶点序号压入拓扑序列的栈 */
124 
125         for (e = GL.vertices[gettop].firstarc; e; e = e->nextarc)
126         {
127             k = e->adjvex;
128             if (!(--GL.vertices[k].in))//将i号顶点的邻接点的入度减1,如果减1后为0,则入栈
129                 stack[++top] = k;
130 
131             if ((etv[gettop] + e->weight)>etv[k])    //求各顶点事件的最早发生时间etv值
132                 etv[k] = etv[gettop] + e->weight;
133         }
134     }
135     std::cout << endl;
136     if (count < GL.vexnum)
137         return ERROR;
138     else
139         return OK;
140 }
141 
142 //求关键路径,GL为有向网,输出G的各项关键活动
143 void CriticalPath(ALGraph GL)
144 {
145     ArcNode *e;
146     int i, gettop, k, j;
147     int ete, lte;                      //声明活动最早发生时间和最迟发生时间变量
148     TopologicalSort(GL);             //求拓扑序列,计算数组etv和stack2的值
149     ltv = new int[GL.vexnum];          //事件最早发生时间数组
150     for (i = 0; i<GL.vexnum; i++)
151         ltv[i] = etv[GL.vexnum - 1];   //初始化
152 
153     std::cout << "etv: ";
154     for (i = 0; i < GL.vexnum; i++)
155         std::cout << etv[i] << "->";
156         std::cout << endl;
157 
158     while (top2 != 0)                  //出栈是求ltv
159     {
160         gettop = stack2[top2--];
161         for (e = GL.vertices[gettop].firstarc; e; e = e->nextarc)//求各顶点事件的最迟发生时间ltv值
162         {
163             k = e->adjvex;
164             if (ltv[k] - e->weight < ltv[gettop])
165                 ltv[gettop] = ltv[k] - e->weight;
166         }
167     }
168 
169     std::cout<<"ltv: ";
170     for (i = 0; i<GL.vexnum; i++)
171         std::cout << ltv[i] << "->";
172     std::cout << endl;
173 
174     for (j = 0; j<GL.vexnum; j++)      //求ete,lte和关键活动
175     {
176         for (e = GL.vertices[j].firstarc; e; e = e->nextarc)
177         {
178             k = e->adjvex;
179             ete = etv[j];              //活动最早发生时间
180             lte = ltv[k] - e->weight;  //活动最迟发生时间
181             if (ete == lte)            //两者相等即在关键路径上
182                 std::cout << "<" << GL.vertices[j].data << " - " << GL.vertices[k].data << ">," << "Length:" << e->weight << endl;
183         }
184     }
185 }
186 int main()
187 {
188     AMGraph G;
189     ALGraph GL;
190     CreateUDN(G);
191     CreateALGraph(G,GL);
192     CriticalPath(GL);
193     return 0;
194 }

测试结果:

猜你喜欢

转载自www.cnblogs.com/Trojan00/p/9021613.html
今日推荐