CHAPTER_10 提高篇(4)——图算法专题
10.7.3关键路径
由于AOE网实际上是有向无环图,而关键路径是图中的最长路径,因此本节实际上给出了一种求解有向无环图中最长路径的方法。
由于关键活动是那些不允许拖延的活动,因此这些活动的最早开始时间必须等于最迟开始时间。因此设置数组e[r]和l[r]分别表示分别表示活动ar的最早开始时间和最迟开始时间。于是,当求出这两个数组之后,就可以通过判断e[r]==l[r]是否成立来确定r是否为关键活动。
如上图所示,事件Vi在经过活动ar之后到达事件Vj。注意到顶点作为事件,也有拖延的可能,因此会存在最早发生时间和最迟发生时间。其中事件的最早发生时间可以理解成旧活动的最早结束时间,事件的最迟发生时间可以理解成新活动的最迟开始时间。设置数组ve[i]和vl[i]分别表示事件 i 的最早发生时间和最迟发生时间,然后求解e[r]和l[r]转换成求解这两个新的数组:
(1)对活动ar来说,只要在事件Vi最早发生时马上开始,就可以使活动ar的开始时间最早,因此e[r]=ve[i]。
(2)如果l[r]是活动ar的最迟发生时间,那么l[r]+length[r]就是事件Vj的最迟发生时间(length[r]表示活动ar的边权)。因此l[r]=vl[j]-length[r]。
于是只要求出ve[]和vl[]数组,就可以通过上面公式得到e[]和l[]数组。
下面讲解如何求解ve[]和vl[]数组:
如下图,有k个事件通过相应的活动到达事件Vj。假设已经算好了事件Vi1~Vik的最早发生时间ve[i1]~ve[ik],那么事件Vj的最早发生事件就是ve[i1]+length[r1]~ve[ik]+length[rk]中的最大值。
因此,想要计算事件Vj的最早发生时间,必须先计算ve[i1]~ve[ik]。我们可以用拓扑排序做到这一点,当按照拓扑排序计算ve[j]时,前驱结点ve[i1]~ve[ik]一定已经计算完成。但是我们又遇到另一个问题,通过前驱结点寻找后继容易,但是通过后继结点寻找前驱较为困难。一个比较好的办法是,在拓扑排序访问到某个结点Vi时,使用ve[i]去更新其所有后继结点的ve值。
求解ve[]部分的代码如下:
//拓扑序列
stack<int> topOrder;
//拓扑排序,顺便求ve数组
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); //将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);
}
//用ve[u]来更新后继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;
}
同理,如下图所示,从时间v1出发通过相应的活动可以到达k个事件Vj1~Vjk。假设已经算好了事件Vj1~Vjk的最迟发生时间vl[j1]~vl[jk],那么事件Vi的最迟发生时间就是vl[j1]-length[r1]~vl[jk]-length[rk]中的最小值。
和ve数组类似,如果需要算出vl[i]的正确值,vl[j1]~vl[jk]必须已经得到。这个要求与ve数组刚好相反,也就是需要在访问某个结点时保证它的后继结点都已经访问完毕,这可以通过拓扑排序的逆序列来实现。在求解ve数组的代码中,我们用stack来存储了拓扑排序,那么只需要按顺序出栈就可以获得逆拓扑序列。
求解vl[]部分的代码如下:
fill(v1,v1+n,ve[n-1]); //初始化数组为终点的ve值
//直接使用topOrder出栈即为逆拓扑序列,求解v1数组
while(!topOrder.empty()) {
int u=topOrder.top();
topOrder.pop();
for(int i=0;i<G[u].size();i++) {
int v=G[u][i].v;
//用u的所有后继结点v的v1值来更新vl[u]
if(vl[v]-G[u][i].w<vl[u]) {
vl[u]=vl[v]-G[u][i].w;
}
}
}
通过上面的步骤已经把求解关键活动的过程倒着推导了一遍,下面给出上面过程的步骤总结,即“先求点,后夹边”:
(1)按照拓扑序列和逆序列分别计算各顶点(事件)的最早发生时间(ve)和最迟发生时间(vl);
(2)用上面的结果计算各边(活动)的最早开始时间(e)和最迟开始时间(l);
(3)e [i->j] == l [i->j]的活动即为关键活动。
主体部分代码如下(适用汇点确定且唯一的情况,以n-1号顶点为汇点为例):
stack<int> topOrder;
//关键路径,不是有向图返回-1,否则返回关键路径长度
int CriticalPath() {
fill(ve,ve+n,0); //初始化ve为0
//拓扑排序,顺便求ve数组
if(topologicalSort()==false)
return -1;
//初始化vl数组
fill(vl,vl+n,ve[n-1]);
//求解vl数组
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;
}
}
}
//遍历邻接表的所有边,计算活动的e和i
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)
cout<<u<<"->"<<v<<endl;
}
}
return ve[n-1];
}
上述代码中,没有将每个活动的 e 和 l 存储下来,是因为一般来说 e 和 l 只是用来判断当前活动的是否为关键活动。如果确实需要存储下每条边的最早开始时间和最迟开始时间,只需要在边的结构体node中添加 e 和 l 域即可。