YAZONG 我的开源

(数据结构)第八讲-图(8.2拓扑排序)

  , ,
0 评论0 浏览

8.2.1 拓扑排序

AOV简介

image.png

计算机专业的各个课程之间有依赖关系。

把课程列表转换为图,其中顶点代表课程,则从V到W有一条边代表V是W的预修课程。

image.png

如何画图呢?先把没有预修课程的四个顶点画出来。

之后,依次画出课程之间的依赖关系。

此图叫做AOV网络,在此有向图中,所有的真实活动是表现在顶点上的,顶点和顶点之间的有向边表示了两个活动之间的先后顺序。

可以把这个问题抽象成一个拓扑排序问题。

image.png

合理的拓扑排序?

图中有环,就意味着V这个活动自己是自己的前驱结点,一定要在自己开始前结束,但这是一个不可能的事情,所以如果一个图中有环,那么就不可能得到一个合理的拓扑排序。

AOV:每个顶点表示的是一个事件或者一个活动。

AOE:每条边表示的是一道工序或者一个活动。

AOV如何产生拓扑排序DAG

image.png

这里给出一个AOV网络,如何把课程进行排序呢?先把无预修课程的课程先输出,其中的课程顺序无所谓。

在把图中的课程输出时,记得把顶点和边都抹掉,重点是要抹掉边。

image.png

依次输出第1、2、3学期的课程。

(为啥C2收收录后,不能直接收录C3和C13,而是需要把其他入度为0的顶点先收录起来呢?看”问题”。)

image.png

依次输出第4、5学期的课程。

image.png

这样就完成了排课的过程。

这个过程实际上是一个拓扑排序的过程,这里输出的是没有前驱顶点的顶点。

如果知道某顶点没有前驱顶点?对于一个有向图来说,每个顶点有两个度,分别是入度和出度,没有前驱顶点的顶点的入度是0,也没有任何边会指向这个顶点。

那么这里选课,输出的同时,就是把没有预修课程的课程从原始图中彻底抹掉,不仅要抹掉顶点,重要的是抹掉边,在抹掉的同时,要把邻接点的入度都-1,邻接点也减少了一条边进来这个邻接点。

把图抹光的时候,一个正常的拓扑排序就产生了。

案例过程(未优化)(O(|V|^2))

image.png

image.png

案例过程(优化)(检测有向无环图)(稀疏图O(|V| + |E|)稠密图O(|V|^2 + |E|))

image.png

任何一个入度为0的顶点,放到一个特殊的容器中,数组、链表、堆栈、队列都行。

在下一次找入度为0的顶点时,就不用去扫描所有的顶点集合,而是去到这个容器中取即可。

那么时间复杂度就变为常数级。

如果要把入度为0的顶点抹掉,那么意味着要把这个顶点的邻接点的入度都要-1,在邻接点-1的同时要把这个邻接点放在容器队列中,下次可以取出使用,否则啥都不做。

image.png

源码(优化)

#include<stdio.h>
/*
图-拓扑排序-邻接表存储-优化-容器收集
*/
int main(void){
	return 0;
}
/* 对Graph进行拓扑排序,  TopOrder[]顺序存储排序后的顶点下标 */
bool TopSort(LGraph Graph, Vertex TopOrder[]){

	int Indegree[MaxVertexNum], cnt;
	Vertex V;
	PtrToAdjVNode W;
	Queue Q = createQueue(Graph->Nv);

	/* 初始化Indegree[] */
	for(V = 0; V < Graph->Nv; V++){
		Indegree[V] = 0;
	}

	/* 遍历图,得到Indegree[] */
	for(V = 0; V < Graph->Nv; V++){
		for(W = Graph->G[V].FirstEdge; W; W = W->Next){
			/* 对有向边<V, W->AdjV>累计终点的入度,在后续逐渐减为0 */
			Indegree[W->AdjV]++;
		}
	}

	/* 将所有入度为0的顶点入列 */
	for(V = 0; V < Graph->Nv; V++){
		if(Indegree[V] == 0){
			AddQ(Q, V);
		}
	}

	/* 下面进入拓扑排序 */
	cnt = 0;
	while(!IsEmpty(Q)){
		/* 弹出一个入度为0的顶点 */
		V = DeleteQ(Q);
		/* 将V存为结果序列的下一个元素 */
		TopOrder[cnt++] = V;
		/* 对V的每个邻接点W->AdjV */
		for(W = Graph->G[V].FirstEdge; W; W = W->Next){
			/* 若删除V使得W->AdjV入度为0,入度逐渐-1 */
			if(--Indegree[W->AdjV] == 0){
				/* 则该顶点入列 */
				AddQ(Q, W->AdjV);
			}
		}
	}

	if(cnt != Graph->Nv){
		/* 说明图中有回路, 返回不成功标志 */
		return false;
	}else{
		return true;
	}

}

8.2.2 关键路径 (拓扑排序重要应用)

AOE简介

image.png

AOV:每个顶点表示的是一个事件或者一个活动。

AOE:每条边表示的是一道工序或者一个活动。

AOE网络一般被用作于安排一个很庞大的项目,这个庞大的项目会分成很多道工序,工序与工序之间是有先后依赖关系的,AOE网络用来解决这类问题。

image.png

正在AOE网络中,顶点Vj表示这个活动,Ai活动是表示在边上,活动达到顶点时表示活动结束。

边分两块:

上块表示活动的持续时间/实际执行时长,表示这个小组完成这道工序一共需要多少天的时间。

上块表示活动的机动时间。

每个顶点分三块:

顶点编号:Vj中的j这个编号。

最早完成时间:所有的任务最早的完成时间。

最晚完成时间:所有的任务最晚的完成时间。

案例过程

image.png

这是整个project,标记了项目开始、项目结束的顶点,每条边代表一件事,每件事情相互依赖的顺序完成之后,到达结束顶点作为结束。

image.png

第一件事:首先要知道每一道工序需要的天数/持续时间是多少。

比如4-6、4-7(仅标记,并不是实际的)开工必须等1-4、2-4完工才能开工。

image.png

更复杂情况,如果4-6、4-7不仅要等1-4、2-4完工,还得等3-5完工才能开工。

现在5-7开工跟1-4、2-4是没关系的,5-7开工只关心3-5。

但4-6、4-7需要等1-4、2-4以及3-5完工才能开工,如何解决?

可以把45顶点合并,画虚边(有箭头,标记4-7需要等3-5完工才能开工),把持续时间/权重标记为0。

这样就可解决4-6、4-7需要等1-4、2-4以及3-5一起完工才能开工。

image.png

先算每个顶点的最早完成时间earliest值:

image.png

初始点的最早完成时间earlier值是0,每次往后面推的时候,如果后面结点的前一步结点的ij是一条边的话,那么前一步结点的earlier值加上这条边的权重,就等于后面结点的earlier值,当earlier值不确定,比如有三条边指向后面结点时,取所有边中最大的时间earlier值,比如结点4,1-2是6+1=7,2-4是4+1=5,最终结点4的earlier值取7。

image.png

作为项目负责人,需要知道哪些组的工期一天都不能耽误,哪些组是中间可以拎出去干其他事情的,也就是可以有机动时间可以拎出去干其他事情。

image.png

倒推开始,先算每个顶点的最晚完成时间latest值:

从最后一个顶点的latest等于earliest,从后往前推每个顶点的latest。如果i结点和j结点之间有一条边的话,那么用j结点的latest减去i结点和j结点之间的持续时间/权重,那么就得到i结点的latest。

当latest值不确定,选择最小的,

比如对于顶点5来说,顶点7可以最晚14天完工,而5-7只需4天就可以完工,那么顶点5只需要在第10天完工即可,但是不行,因为5-4这条边还在,所以顶点5必须在第7天开工和完工,否则整个工期就要延误,所以二者必须选择最小的latest值,那么0-1、0-2、0-3同理。

那么这里,比如0-1、1-1这些组的工期是一天都不能耽误的。

image.png

此时算机动时间:哪些组是中间可以拎出去干其他事情的,也就是可以有机动时间可以拎出去干其他事情。

image.png

比如0-2,结点i和结点j之间有一条边的话,用结点j的latest:6减掉结点i的earliest:0,再减去结点i和结点j之间边的持续时间/权重:4,就等于机动时间2,这里结点i到结点j只需要4天就可以完工,另外2天为机动时间,可以另外做别的事情,那么2-4、5-7同理。

重要/最终概念

image.png


标题:(数据结构)第八讲-图(8.2拓扑排序)
作者:yazong
地址:https://blog.llyweb.com/articles/2023/10/22/1697966178488.html