图算法专题(四)【拓扑排序】

图算法专题四

  • 拓扑排序
    • 有向无环图
    • 拓扑排序
  • 关键路径
    • AOV网和AOE网
    • 最长路径
    • 关键路径

拓扑排序

有向无环图

  如果一个有向图的任意顶点都无法通过一些有向边回到自身,那么称这个有向图为有向无环图(DAG)。

拓扑排序

  拓扑排序是将有向无环图G的所有顶点排成一个线性序列,使得对图G中的任意两个顶点u、v,如果存在边u->v,那么u在序列中一定在v的前面。这个序列又被称为拓扑序列

  如何求拓扑序列?

  1. 定义一个队列Q,并把所有入度为0的结点加入队列。
  2. 取队首结点,输出。然后删去所有从它出发的边,并令这些边到达的顶点的入度-1,如果某个顶点的入度减为0,则将其加入队列。
  3. 反复进行2操作,直到队列为空。如果队列为空时入过队的结点数目刚好为N,说明拓扑排序成功,图G为有向无环图;否则拓扑排序失败,G为有环图。

  用邻接表实现拓扑排序。显然由于需要记录结点的入度,因此需要额外建立一个数组inDegree[MAXV],并在程序一开始读入图时就记录好每个结点的入度。

#include
using namespace std;
const int MAXV = 110;

vector<int> G[MAXV];//邻接表
int n,m,inDegree[MAXV]//顶点数、入度
//拓扑排序
bool topologicalSort(){
	int num =0;//记录加入拓扑序列的顶点数
	queue<int> q;
	for(int i =0;i<n;i++){
		if(inDegree[i] == 0){
			q.push(i);//将所有入度为0的顶点入队 
		}
	} 
	while(!q.empty()){
		int u = q.front();//取队首顶点u 
		//printf("%d",u);//可以输出u作为拓扑序列的顶点 
		q.pop();
		for(int i =0;i<G[u].size;i++){
			int v = G[u][i];//u的后继结点v
			inDegree[v]--;//v的入度-1
			if(inDegree[v] == 0){
				q.push(v);//顶点v的入度减为0则入队 
			} 
		}
		G[u].clear();//清空顶点u的所有边
		num++;//加入拓扑序列的顶点数+1 
	}
	if(num == n){
		return true;//拓扑排序成功 
	}else{
		return false;//失败,有环 
	}
} 

  拓扑排序很重要的应用就是判断一个给定的图是否是有向无环图。如果上述函数返回true,说明是有向无环图。

  如果要求有多个入度为0的结点,选择编号最小的结点,那么把queue改成priority_queue,并保持队首元素是优先队列中最小的元素(greater)即可。【或者使用set也可以】
  
  

关键路径

AOV网和AOE网

  顶点活动(AOV)网是指用顶点表示活动,而用边集表示活动间优先关系的有向图。显然图中不应当出现有向环。

  边活动(AOE)网是指用带权的边集表示活动,而用顶点表示事件的有向图,其中边权表示完成活动需要的时间。“事件”仅代表一个中介状态。

  一般来说,AOE网是用来表示一个工程的进行过程,而工程常常可以分为若干个子工程(即“活动”),显然AOE网不应当有环。考虑到对工程来说总会有一个起始时刻和结束时刻,因此AOV网一般只有一个源点(即入度为0的点)和一个汇点(即出度为0的点)。但如果有多个源点和汇点,我们可以通过添加一个“超级源点”和“超级汇点”的方法来使源点和汇点唯一,即从超级源点出发,连接所有入度为0的点;从所有出度为0的点出发连接超级汇点;添加的有向边的边权均为0。

  如果给定AOV网中各顶点活动所需要的时间,那么就可以将AOV网转换为AOE网。比较简单的方法是:将AOV网中每个顶点都拆成两个顶点,分别表示活动的起点和终点,而两个顶点之间用有向边连接,该有向边表示原顶点的活动,边权给定;原AOV网中的边全部视为空活动,边权为0。

  AOE网中的最长路径被称为关键路径关键路径就是AOE网的最长路径),而把关键路径上的活动成为关键活动,显然关键活动会影响整个工程的进度。

最长路径

  对一个没有正环的图(指从源点可达的正环),如果需要求最长路径长度,则可以把所有边的边权乘以 -1,令其变为相反数,然后使用bellman-Ford算法或SPFA算法求最短路径长度,将所得结果取反即可。注意:此处不能使用Dijkstra算法。

  最长路径问题,寻求的是图中的最长简单路径

  而如果求有向无环图的最长路径长度,则使用求关键路径的求法更快。

关键路径

  求解有向无环图(DAG)中最长路径的方法。

  关键路径是指那些不允许拖延的活动,因此这些活动的最早开始时间必须=最晚开始时间。因此可以设置数组e和l,其中e[r]和l[r]分别表示活动ar的最早开始时间和最迟开始时间。于是,求出这两个数组之后,就可以通过判断e[r] == l[r]是否成立来确定活动r是否是关键活动。

  如何求解数组e和l?

  事件的最早开始时间可以理解为旧活动的最早结束时间,事件的最迟发生时间可以理解成新活动的最迟开始时间。设置数组ve和vl,其中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的边权)。因此l[r] = vl[j]-length[r]。

  于是只要求出ve和vl这两个数组,就可以通过上面的公式得到e和l这两个数组。
图算法专题(四)【拓扑排序】_第1张图片
  ve[j] = max{ ve[ip]+length[rp] } (p = 1…k)
因为只有所有能到达vj的活动都完成之后,vj才能被激活。

  想获得ve[j]的正确值,还需要得到前面的ve。即要做到,在访问某个结点前,其前驱结点都已被访问完毕。因此使用拓扑排序。让拓扑排序访问到Vj的时候,Vi1~ Vik一定都已经用来更新过ve[j],此时的ve[j]便是正确值,就可以用它去更新Vj后面的所有后继结点的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]来更新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;
} 

图算法专题(四)【拓扑排序】_第2张图片
  ve[i] = min{ vl[jp]-length[rp] } (p=1…k)
必须保证Vj1~ Vjk的最迟发生时间能被满足。

  与ve数组类似,要求vl的正确值需要vl[j1]vl[jk]的值已求出。即需要在**访问某个结点时保证它的后续结点都已经被访问完毕**,通过**逆拓扑序列**来实现。可以通过颠倒拓扑序列来得到一组合法的逆拓扑序列。因为上述使用栈来存放拓扑序列,因此**只要按顺序出栈就是逆拓扑序列**。当访问逆拓扑序列中的每个书剑Vi时,就可以遍历Vi的所有后继结点Vj1~~ Vjk,使用vl[j1]~vl[jk]来求出vl[i]。

fill(vl,vl+n,ve[n-1]);//vl数组初始化,初始值为终点的ve值
//直接使用topOrder出栈即为逆拓扑序列,求解vl数组
while(!topOrder.empty()){
	int u = topOrder.top();//栈顶元素u
	topOrder.pop();
	for(int i =0;i<G[u].size();i++){
		int v = G[u][i].v;
		//用u的所有后继结点v的vl值来更新vl[u]
		if(vl[v]-G[u][i].w < vl[u]){
			vl[u] = vl[v]-G[u][i].w;
		} 
	} 
} 

  下面给出上面过程的步骤总结,即“先求点,再夹边”:

  1. 按拓扑序和逆拓扑序分别计算各顶点(事件)的最早发生时间和最迟发生时间:
      最早(拓扑序):ve[j] = max{ ve[i]+length[i->j] }
      最晚(逆拓扑序):vl[i] = min{ vl[j] - length[i->j] }
  2. 用上面的结果计算各边(活动)的最早开始时间和最晚开始时间:
      最早: e[i->j] = ve[i]
      最晚: l[i->j] = vl[j] - length[i->j]
  3. e[i->j] == l[i->j]的活动即为关键活动。
      
      下面是部分主体代码(适用于汇点确定且唯一的情况,以n-1号顶点为汇点):
//计算点的事件的最早开始时间
//拓扑序列
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]来更新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;
} 

//关键路径,不是有向无环图返回-1,否则返回关键路径长度
int criticalPath(){
	memset(ve,0,sizeof(ve));//ve数组初始化 
	if(topologicalSort() == false){
		return -1;//不是有向无环图 
	}
	fill(vl,vl+n,ve[n-1]);//vl数组初始化,初始值为终点的ve值
	//直接使用topOrder出栈即为逆拓扑序列,求解vl数组
	while(!topOrder.empty()){
		int u = topOrder.top();//栈顶元素u
		topOrder.pop();
		for(int i =0;i<G[u].size();i++){
			int v = G[u][i].v;
			//用u的所有后继结点v的vl值来更新vl[u]
			if(vl[v]-G[u][i].w < vl[u]){
				vl[u] = vl[v]-G[u][i].w;
			} 
		} 
	}
	//遍历邻接表的边,计算活动的最早开始时间和最晚开始时间
	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;
			//活动的最早开始时间e和最晚开始时间l
			int e = ve[u], l = vl[v]-w;
			//如果e==l,说明活动u->v是关键活动
			if(e == l){
				printf("%d->%d\n",u,v);//输出关键活动 
			} 
		}
	}
	return ve[n-1];//返回关键路径长度 
} 

  如果事先不知道汇点编号,可以取ve数组的最大值。于是只用在fill函数之前添加一小段语句,然后改变下vl函数初始值即可:

int maxLength = 0;
for(int i =0;i<n;i++){
	if(ve[i] >maxlength){
		maxlength = ve[i];
	}
}
fill(ve,ve+n,maxlength);

你可能感兴趣的:(模板)