《算法笔记》读书记录DAY_52

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 域即可。

猜你喜欢

转载自blog.csdn.net/jgsecurity/article/details/121203042