【数据结构与算法】拓扑排序与关键路径

125 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第10天,点击查看活动详情

🔥 本文由 程序喵正在路上 原创,在稀土掘金首发!
💖 系列专栏:数据结构与算法
🌠 首发时间:2022年12月4日
🦋 欢迎关注🖱点赞👍收藏🌟留言🐾
🌟 一以贯之的努力 不得懈怠的人生

有向无环图描述表达式

有向无环图(DAG)

如果一个有向图中不存在环,则称之为有向无环图,简称 DAGDAG 图(Directed Acyclic GraphDirected \ Acyclic \ Graph),比如下面这个图

image.png

DAG 描述表达式

有下面这样一个表达式:

((a+b)(b(c+d))+(c+d)e)((c+d)e)((a + b) * (b * (c + d)) + (c + d) * e) * ((c + d) * e)

我们可以将其表示成树的形式:

image.png

但是我们会发现,树中有些地方是重复的,比如 (c+d)e(c + d) * e 这部分,所以我们删除一部分,变成

image.png

接着我们发现还有 c+dc + d 这部分也重复了,也得删去

image.png

最后还有 bb 也重复

image.png

规律:顶点中不可能出现重复的操作数

解题方法

  1. 把各个操作数不重复地排成一排

image.png

  1. 标出每个运算符的生效顺序(先后顺序有点出入无所谓)

image.png 5. 按顺序加入运算符,注意 “分层”

image.png 7. 从底向上逐层检查同层的运算符是否可以合体

image.png

拓扑排序

AOV网

AOVAOV 网(Activity On Vertex NetWorkActivity \ On \ Vertex \ NetWork,用顶点表示活动的网):用 DAGDAG 图(有向无环图)表示一个工程。顶点表示活动,有向边 <Vi,Vj><V_i, V_j> 表示活动 ViV_i 必须先于活动 VjV_j 进行

拓扑排序

在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,称为该图的一个拓扑排序:

  • 每个顶点出现且只出现一次
  • 若顶点 AA 在序列中排在顶点 BB 的前面,则在图中不存在从顶点 BB 到顶点 AA 的路径

每个 AOVAOV 网都有一个或多个拓扑排序序列

拓扑排序的实现:

  1. AOVAOV 网中选择一个没有前驱(入度为 00)的顶点并输出
  2. 从网中删除该顶点和所有以它为起点的有向边
  3. 重复 1122 直到当前的 AOVAOV 网为空或者当前网中不存在无前驱的顶点为止
#define MaxVertexNum 100				//图中顶点数目的最大值
typedef struct ArcNode {				//边表结点
	int adjvex;							//该弧所指向的顶点的位置
	struct ArcNode *nextarc;			//指向下一条弧的指针
	//InfoType info;					//网的边权值
}ArcNode;

typedef struct VNode {					//顶点表结点
	VertexType data;					//顶点信息
	ArcNode *firstarc;					//指向第一条依附该顶点的弧的指针
}VNode, AdjList[MaxVertexNum];

typedef struct {
	AdjList vertices;					//邻接表
	int vexnum, arcnum;					//图的顶点数和弧数
}Graph;									//Graph是以邻接表存储的图类型

bool TopologicalSort(Graph G) {
	InitStack(S);				//初始化栈,存储入度为0的顶点
	for (int i = 0; i < G.vexnum; ++i) {
		if (indegree[i] == 0) Push(S, i);		//将所有入度为0的顶点进栈
	}
	int count = 0;				//计数,记录当前已经输出的顶点数
	while (!IsEmpty(S)) {		//栈不空,则存在入度为0的顶点
		Pop(S, i);				//栈顶元素出栈
		print[count++] = i;		//输出顶点i
		
		for (p = G.vertices[i].firstarc; p; p = p->nextarc) {
			//将所有i指向的顶点的入度减1,并且将入度减为0的顶点压入栈S
			v = p->adjvex;
			if (!(--indegree[v])) Push(S, v);	//入度为0,则入栈
		}//for
	}//while

	if (count < G.vexnum) return false;		//排序失败,有向图中有回路
	else return true;						//拓扑排序成功

其中 indegree 是记录各个顶点入度的数组,初始化为各顶点的入度;print 是记录拓扑序列的数组,初始化为 1-1

在这个拓扑排序中,每个顶点和每条边都需要处理一次,整体的时间复杂度为 O(V+E)O(|V| + |E|),如果是采用邻接矩阵存储的图,则需要 O(V2)O(|V|^2) 的时间复杂度

逆拓扑排序

逆拓扑排序的实现:

  1. AOVAOV 网中选择一个没有后继(出度为 00)的顶点并输出
  2. 从网中删除该顶点和所有以它为终点的有向边
  3. 重复 1122 直到当前的 AOVAOV 网为空

逆拓扑排序的实现(DFS算法)

void DFSTraverse(Graph G) {				//对图G进行深度优先遍历
	for (v = 0; v < G.vexnum; ++v) 		//初始化已访问标记数据
		visited[v] = false;
		
	for (v = 0; v < G.vexnum; ++v)
		if (!visited[v]) DFS(G, v);
}

void DFS(Graph G, int v) {				//从顶点v开始,深度优先遍历图G
	visited[v] = true;					//设置已访问标记
	for (w = FirstNeighbor(G, v); w >= 0; w = NextNeighbor(G, v, w))
		if (!visited[w]) DFS(G, w);
	
	print(v);			//输出顶点
}

如果图中存在回路,那么上面的程序就不再使用,我们需要给每个顶点添加一个是否已经入栈的标记来判断是否有回路即可

关键路径

AOE网

在带权有向图中,以顶点表示事件,以有向边表示活动,以边上的权值表示完成该活动的开销(如完成该活动所需的时间),称之为哟个边表示活动的网络,简称 AOEAOE 网(Activity On Edge NetWorkActivity \ On \ Edge \ NetWork

image.png

AOEAOE 网具有以下两个性质:

  • 只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始
  • 只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生。另外,有些活动是可以并行进行的

AOEAOE 网中仅有一个入度为 00 的顶点,称为开始顶点(源点),它表示整个工程的开始;也仅有一个出度为 00 的顶点,称为结束顶点(汇点),它表示整个工程的结束

从源点到汇点的有向路径可能有多条,所有路径中,具有最大路径长度的路径称为关键路径,而把关键路径上的活动称为关键活动

完成整个工程的最短时间就是关键路径的长度,若关键活动不能按时完成,则整个工程的完成时间就会延长

事件 vkv_k 的最早开始时间 ve(k)ve(k) —— 决定了所有从 vkv_k 开始的活动能够开工的最早时间

活动 aia_i 的最早开始时间 e(i)e(i) —— 指该活动弧的起点所表示的事件的最早发生时间

事件 vkv_k 的最迟开始时间 vl(k)vl(k) —— 它是指在不推迟整个工程完成的前提下,该事件最迟应该发生的时间

活动 aia_i 的最迟开始时间 l(i)l(i) —— 它是指该活动弧的终点所表示事件的最迟发生时间与该活动所需时间之差

活动 aia_i 的时间余量 d(i)=l(i)e(i)d(i) = l(i) - e(i),表示在不增加完成整个工程所需总时间的情况下,活动 aia_i 可以拖延的时间;若一个活动的时间余量为零,则说明该活动必须要如期完成,d(i)=0d(i) = 0l(i)=e(i)l(i) = e(i) 的活动是关键活动,由关键活动组成的路径就是关键路径

求关键路径的步骤

  1. 求所有事件的最早发生时间 ve()ve()
  2. 求所有事件的最迟发生时间 vl()vl()
  3. 求所有活动的最早发生时间 e()e()
  4. 求所有活动的最迟发生时间 l()l()
  5. 求所有活动的时间余量 d()d()

image.png

① 求所有事件的最早发生时间 ve()ve()

按拓扑排序序列,依次求各个顶点的 ve(k)ve(k)

  • veve( 源点 ) =0= 0
  • ve(k)=Max{ve(j)+Weight(vj,vk)}ve(k) = Max\{ve(j) + Weight(v_j, v_k)\}vjv_jvkv_k 的任意前驱

上图的拓扑序列为:V1V3V2V5V4V6V_1、V_3、V_2、V_5、V_4、V_6

那么

ve(1)=0ve(1) = 0

ve(3)=2ve(3) = 2

ve(2)=3ve(2) = 3

ve(5)=6ve(5) = 6

ve(4)=max{ve(2)+2,ve(3)+4}=6ve(4) = max\{ve(2) + 2, ve(3) + 4\} = 6

ve(6)=max{ve(5)+1,ve(4)+2,ve(3)+3}=8ve(6) = max\{ve(5) + 1, ve(4) + 2, ve(3) + 3\} = 8

② 求所有事件的最迟发生时间 vl()vl()

按逆拓扑排序序列,依次求各个顶点的 vl(k)vl(k)

  • vlvl(汇点) == veve(汇点)
  • vl(k)=Min{vl(j)+Weight(vk,vj)}vl(k) = Min\{vl(j) + Weight(v_k, v_j)\}vjv_jvkv_k 的任意后继

上图的逆拓扑序列为:V6V5V4V2V3V1V_6、V_5、V_4、V_2、V_3、V_1

那么

vl(6)=8vl(6) = 8

vl(5)=7vl(5) = 7

vl(4)=6vl(4) = 6

vl(2)=min{vl(5)3,vl(4)2}=4vl(2) = min\{vl(5) - 3, vl(4) - 2\} = 4

vl(3)=min{vl(4)4,vl(6)3}=2vl(3) = min\{vl(4) - 4, vl(6) - 3\} = 2

vl(1)=min{vl(2)3,vl(3)2}=0vl(1) = min\{vl(2) - 3, vl(3) - 2\} = 0

③ 求所有活动的最早发生时间 e()e()

若边 <vk,vj><v_k, v_j> 表示活动 aia_i,则有 e(i)=ve(k)e(i) = ve(k),说白了就等于活动 aia_i 所在弧的弧尾的事件的最早发生时间 ve(k)ve(k),所以

e(1)=ve(1)=0, e(2)=ve(1)=0, e(3)=ve(2)=3, e(4)=4, e(5)=ve(3)=2, e(6)=2, e(7)=6, e(8)=6e(1) = ve(1) = 0, \ e(2) = ve(1) = 0, \ e(3) = ve(2) = 3, \ e(4) = 4, \ e(5) = ve(3) = 2, \ e(6) = 2, \ e(7) = 6, \ e(8) = 6

④ 求所有活动的最迟发生时间 l()l()

若边 <vk,vj><v_k, v_j> 表示活动 aia_i,则有 l(i)=vl(j)Weight(vk,vj)l(i) = vl(j) - Weight(v_k, v_j),所以

l(1)=vl(2)3=1, l(2)=vl(3)2=0, l(3)=4, l(4)=4, l(5)=2, l(6)=5, l(7)=6, l(8)=7l(1) = vl(2) - 3 = 1, \ l(2) = vl(3) - 2 = 0, \ l(3) = 4, \ l(4) = 4, \ l(5) = 2, \ l(6) = 5, \ l(7) = 6, \ l(8) = 7

⑤ 求所有活动的时间余量 d()d()

d(i)=l(i)e(i)d(i) = l(i) - e(i)

方面a1a_1a2a_2a3a_3a4a_4a5a_5a6a_6a7a_7a8a_8
e(k)e(k)00332266
l(k)l(k)10442567
d(k)d(k)10110301

关键活动:a2a5a7a_2、a_5、a_7

关键路径:V1>V3>V4>V6V_1 -> V_3 -> V_4 -> V_6

关键活动、关键路径的特性

  • 若关键活动耗时增加,则整个工程的工期将增长
  • 缩短关键活动的时间,可以缩短整个工程的工期
  • 当缩短到一定程度时,关键活动可能会变成非关键活动
  • 可能有多条关键路径,只提高一条关键路径上的关键活动并不能缩短整个工程的工期,只有加快那些包括在所有关键路径上的关键活动才能达到缩短工期的目的