在之前的一篇文章,提到了拓扑排序的相关概念,其实拓扑排序所操作的对象类型就是AOV网。
先介绍一下AOV网和AOE网的相关概念:
AOV网,也就是顶点活动,就是前面文章所阐述的一种有向无环图。其节点代表活动,使用边来代表节点之间的序列关系;
AOE网,也就是边活动,其用边来代表具体的活动,并且用定点代表事件,其中边权代表完成活动的时间。这里要区分事件和活动这一概念。这里的事件更像是一种标志,标志到这个节点时,之前的活动都已经结束,可以进行下一个活动,就像书中所说,其代表的是一个中介状态。对于AOE网来说,所有的起始时间和最终事件都可以归为一个点。
此时,就引入关键路径的概念;
所谓关键路径,就是AOE网中最长的路径,关键路径上的活动称为关键活动,关键活动就是不允许拖延的活动。关键路径下的时长也成为最短时长。这里需要注意一点,活动的进行是多线程的,并不是单线程,所以在多线程情况下的最短时间映射的就是关键路劲,也就是图中的最长路径,这样谈这个概念才是有意义的,否则是无意义的;
所以本质上关键路径的求解就是对于有向无环图DAG中最长路径的求解方法;
由于活动时具有拖延性的,每一个活动都有最早的发生时间和最晚发生时间,其时差就可以任意安排的时间。因此对于关键路径中的关键活动,其标志性的特点就是:最早发生时间=最晚发生时间;
对该条件进行抽象,就是对于两个数组e[r],l[r],分别代表相应活动的最早发生时间和最晚发生时间,其中当e[i]=l[i],就代表该活动位关键活动,也就是关键路径上的一条边;
接下来关键的就是如何对这两个数组进行求解:
在书上,求解方法是构造节点的相应数组,来进行数组e,l的求解;
由于节点作为时间,也有拖延的可能性,其也存在最早和最晚时间。所以事件的最早开始时间就是上一个活动的最早结束时间;事件的最晚开始事件就是新活动的最迟开始时间。
所以对于数组e,l,求解就转换为了对另外两个数组的求解:ve,vl;
如上图所示:
对于活动Ar来说,其最早开始时间就是事件Vi的最早开始时间,因此:e[r]=ve[i];
活动事件Vj的最晚开始时间取决于活动Ar的最晚开始时间,也就是vl[j]=l[r]+length[r],也就是l[r]=vl[j]-length[r];
此时,e,l数组的求解也就变成了vl,ve数组的求解;
对于数组vl,ve,可以借助前驱节点和后继节点来进行判断;
对于ve数组,也就是最早发生数组,可以采用如下的判断方式:
如上所示,如果Vj要发生,则前驱节点必须要全部发生,并且前驱结点的发生时间也为最早发生时间。由于各个路线的权值不一样,也就是活动时长不一样,所Vj的发生时间必为Max(ve[ip]+length[rp]);
所以这种判定方式的实现也十分简单,只要使用拓扑排序就可以保证访问Vj使前面的所有节点都进行过访问;对于代码上的逻辑实现,我们每次找到一个节点之后,进行到后继节点的比较,始终让他更新后继节点的值,从而使得遍历完i前驱节点之后,j节点的值已经是Max(ve[ip]+length[rp]);
相关的判定代码如下所示:
stack<int> topOrder;//拓扑序列
bool topologicalSort(){
queue<int>q;
for(int i=0;i<n;i++){
if(inDegree[i]==0){
q.push(i);
}
}
while(!q.empty()){
int u=q.front();
q.pop();
topOrder.push(u)//加入拓扑序列;
for(int i=0;i<G[u].size();i++){
int v=G[u][i].v;
inDegree[v]--;
if(inDegree[v]==0){
q.push(v);
}
if(ve[u]+G[u][i].w>ve[v]){
ve[v]=ve[u]+G[u][i].w;
}
}
}
if(topOrder.size()==n)
return true;
else
return false;
}
其中值得更新后继节点的就是该部分:
注意,这里G是一个二维数组,G[u]代表的是u节点,G[u]表示一个数组,都为u的后继节点;
使用上述代码就可以使得ve[j]的值由不同的ix节点遍历时,不断进行更新。当ix节点全部访问完就可以将其入队列,访问j节点;
接下来就是对vl数组进行构造和操作:
vl数组的公式推导为:l[r]=vl[j]-length[r]
由于vl数组代表的当前事件发生的最晚时间,对于该问题求解,我们可以转换为向前求解,也就是j事件的最晚事件-活动时间的最小值,就是i节点的最晚发生时间,也就是:Min{vl[jp]-length[rp]}
vl数组所对应的情况如下所示:
因此,vl数组的操作必须要先知道后继节点的值。对于该问题,我们可以使用逆拓扑推导,但是这里时根据ve数组压栈得到的逆序列,所以很容易实现;
大致代码如下所示:
fill(vl,vl+n,ve[n-1]);//使用终点的ve值
while(!topOrder.empty()){
int u=topOrder.top();
topOrder.pop();
for(int i=0;i<G[u].size();i++){
int v=G[u][i].v;
if(vl[v]-G[u][i].w<vl[u]){
vl[u]=vl[v]-G[u][i].w;
}
}
}
注意一点,先进行正拓扑排序,在进行逆拓扑排序,并且第二次使用的最后初值为ve的最终值;
当vl和ve数组求解完毕,就可以根据公式来进行e和v的求解。
注意,这里也要分清一个问题:e和v数组针对的时边,也就是活动,而ve,vl数组针对的时点,两者不同。
for(int u=0;u<n;u++){
for(int i=0;i<G[u].size();i++){
int v=G[u][i].v,w=G[u][i].w;
int e=ve[u];
l=vl[v]-w;
if(e==l){
//说明时关键活动,也就是关键路径
//输出
}
}
}
当然,如果只需要求最长的路径长度,直接取ve数组的最大值就可以,因为ve数组的最大值代表的必定是最后一个事件发生的最早时间(也就是完成事件的最早发生时间),所以也就是最快的事件,最长路径的长度;