我勒个去,毕业论文终于提交了,赶紧完成图基本算法的最后一节。。。
1 定义
在一个表示工程的带权有向图中,用顶点表示事件,用有向边表示活动,用边上的权值表示活动的持续时间,这种有向图的边表示活动的网,称为AOE网。AOE网中没有入边的顶点称为始点或源点,没有出边的顶点称为终点或汇点。由于一个工程通常总有一个开始和一个结束,因此AOE网只有一个源点和汇点。
与AOV网区别在于,AOV网是顶点表示活动的网,它只描述活动之间的制约关系,而AOE网是用边表示活动的网,边上的权值表示活动持续的时间。其是建立在活动之间制约关系没有矛盾的基础之上,再来分析整个工程需要多少时间。
我们把路径上各个活动所持续的时间之和称为路径长度,从源点到汇点具有最大长度的路径叫关键路径,在关键路径上的活动叫关键活动。只有找出AOE网中的关键路径,并缩短路径上关键活动的工作时间,才能最有效的缩短完成工程的时间。
2 建立测试网
按照拓扑排序的邻接表结构,建立测试网:
class vex{
constructor(value){
this.data = value;
this.firstEdge = null;
this.in = 0; //用于存放顶点的入度
}
}
class adjvex{
constructor(node,weight){
this.node = node;
this.weight = weight;
this.next = null;
}
}
class Graph{
constructor(v,vr){
let len = v.length;
let vexs = new Array(len);
let v1=0,v2=0;
let newvex = null;
for (let i=0;i<len;i++){
vexs[i] = new vex(v[i]);
}
for (let arc of vr){
v1 = v.indexOf(arc[0]);
v2 = v.indexOf(arc[1]);
newvex = new adjvex(v2,arc[2]);
newvex.next = vexs[v1].firstEdge; //头插法
vexs[v1].firstEdge = newvex;
vexs[v2].in++;
}
this.adjList = vexs;
}
}
let a = new Graph(['v0','v1','v2','v3','v4','v5','v6','v7','v8','v9','v10','v11','v12','v13'],[['v0','v11',1],['v0','v4',1],['v0','v5',1],['v1','v4',1],['v1','v8',1],['v1','v2',1],['v2','v5',1],['v2','v6',1],['v3','v2',1],['v3','v13',1],['v4','v7',1],['v5','v8',1],['v5','v12',1],['v6','v5',1],['v8','v7',1],['v9','v11',1],['v9','v10',1],['v10','v13',1],['v12','v9',1]]);
console.log(a);
3 算法思路
在AOE网中找到关键路径在于如何找到关键活动。对此我们定义几个参数:
- 事件的最早发生时间etv:即顶点代表的事件最早发生的时间
- 事件的最晚发生时间ltv:即顶点代表的事件最晚发生的时间
- 活动最早的开工时间ete:即弧代表的活动最早发生时间
- 活动最晚的开工时间lte:即弧代表的活动最晚发生时间,也就是不推迟工期的最晚开工时间。
例如下图,有4个事件: v0,v1,v2和v3,以及4个活动:a0,a1,a2和a4。事件v3必须等到活动a2和a4完成以后才能触发,因此在不推迟工期的条件下,事件v3的最早发生时间和最晚发生时间都是12,且v2的最早和最晚发生时间也都是4,但是对于事件v1,其只要保证在时间7之前发生即可,因此其最早发生时间是3,而最晚发生时间是7。综上所述,要减少整个工程的时间,对于缩减活动a0和a2的时间并没有什么作用,关键要缩减经过事件顶点v2的活动a1和a4的时间。因此经过顶点v2的路径才是关键路径。由此我们可以看出,一个活动是否为关键活动在于活动最早的开工时间ete是否等于活动最晚的开工时间lte,若等于,该活动则为关键活动,否则则不是。
在关键路径算法,我们先求得顶点事件的最早和最晚发生时间,再求得弧活动的最早和最晚开工时间,最后比较弧活动的两者时间是否相等,判断是否为关键活动。
4 代码
整个求解关键路径过程可划分为三部分:
- 求解各顶点事件的最早发生时间
- 求解各顶点事件的最晚发生时间
- 求解各弧上活动的最早和最晚发生时间,并做关键活动的判断
首先求得各顶点事件的最早发生时间,可以看到代码与拓扑排序很相似。求各顶点的最早发生时间判定条件如下
例如下图,etv[1] + a2 < etv[2] + a4,所以etv[3] = 12。
function getEtvs(G){
let etvs = []; //用于存储各顶点的最早发生时间
let stack = [], T = []; //stack为辅助栈,T为存储拓扑序列的数组
let count = 0; //用于统计顶点个数
for (let i=0;i<G.adjList.length;i++){ //初始化数组,并将入度为0的顶点推入栈
etvs[i] = 0;
if (G.adjList[i].in === 0){
stack.push(i);
}
}
let currentAdjVex = null;
let currentIndex = 0;
while(stack.length > 0){
currentIndex = stack.pop(); //弹出栈顶入度为0的顶点
count++;
T.push(currentIndex);
currentAdjVex = G.adjList[currentIndex].firstEdge;
while(currentAdjVex){ //遍历当前顶点的所有邻接顶点
if (etvs[currentIndex] + currentAdjVex.weight > etvs[currentAdjVex.node]){ //关键代码,求各顶点事件最早发生时间
etvs[currentAdjVex.node] = etvs[currentIndex] + currentAdjVex.weight;
}
if (--G.adjList[currentAdjVex.node].in === 0){ //将当前邻接顶点入度减1,若等于0,则推入栈中
stack.push(currentAdjVex.node);
}
currentAdjVex = currentAdjVex.next;
}
}
if (count < G.adjList.length){
return false;
}else{
return [etvs,T];
}
}
接着根据各顶点事件的最早发生时间,反向求各顶点的最晚发生时间,相当于把拓扑排序倒过来,判定条件如下
例如下图,顶点v6的最晚发生时间为25,而顶点v7为19,ltv[6] - 9 > ltv[7] -4,为了保证工程不延期,因此顶点v4最晚发生时间ltv[4] = 15
function getLtvs(G,etvs,T){
let ltvs = [];
for (let i=0;i<G.adjList.length;i++){ //初始化每个顶点的最晚发生时间
ltvs[i] = etvs[G.adjList.length-1];
}
let currentAdjVex = null;
let currentIndex = 0;
while(T.length > 0){
currentIndex = T.pop(); //反向拓扑序列计算
currentAdjVex = G.adjList[currentIndex].firstEdge;
while(currentAdjVex){
if (ltvs[currentIndex] > ltvs[currentAdjVex.node] - currentAdjVex.weight){ //关键代码,求各顶点事件的最晚发生时间
ltvs[currentIndex] = ltvs[currentAdjVex.node] - currentAdjVex.weight;
}
currentAdjVex = currentAdjVex.next;
}
}
return ltvs;
}
最后即可根据各顶点事件的最早和最晚发生时间,计算弧上活动的最早和最晚时间,并作比较判断是否为关键活动。注意的是,弧上活动<vi,vj>
的最早开工时间不可能早于顶点vi
事件最早发生时间,而最晚开工时间则不可能晚于顶点vj
事件最晚发生时间 - 弧上活动持续时间。因此代码如下:
function criticalPath(G){
let etvs = null, ltvs = null;
let T = null;
[etvs,T] = getEtvs(G); //调用函数获取图各顶点事件的最早和最晚发生时间
ltvs = getLtvs(G, etvs, T);
let ete = 0, lte = 0;
let currentAdjVex = null;
for (let i=0;i<G.adjList.length;i++){ //遍历每一条边
currentAdjVex = G.adjList[i].firstEdge;
while(currentAdjVex){
ete = etvs[i]; //弧上活动的最早开工时间
lte = ltvs[currentAdjVex.node] - currentAdjVex.weight; //弧上活动的最晚开工时间
if (ete === lte){
console.log('v'+i,'v'+currentAdjVex.node,currentAdjVex.weight);
}
currentAdjVex = currentAdjVex.next;
}
}
}