一、有向无环图描述表达式
1、概念:
有向无环图(Directed Acyclic Graph):若一个有向图中不存在环,则称为有向无环图,简称DAG。有向无环图是描述含有公共子式的表达式的有效工具。
2、示例:
例如表达式:((a+b)×(b×(c+d))+(c+d)×e)×((c+d)×e),可以用有向二叉树来表示,如下图中的左图所示。但仔细观察该表达式,可以发现有一些相同的子表达式,例如(c+d)和(c+d)×e,而在对应的二叉树中,它们也重复出现。
若利用有向无环图,则可实现对相同子式的共享,从而节省存储空间、去除重复的顶点(使顶点个数达到最少),下图中的右图为该表达式的有向无环图表示。
【注意:在表达式的有向无环图表示中,不可能出现重复的操作数顶点。】
下面给出求表达式((a+b)×(b×(c+d))+(c+d)×e)×((c+d)×e)的有向无环图的详细过程:
1)将表达式中的操作数不重复的排在第0层:
2)标出表达式中的运算符的生效次序:
3)按顺序将运算符加入有向无环图中:(注意进行分层)
3、例题:
A
二、拓扑排序
1、AOV网和拓扑排序的概念:
1)AOV网:若用有向无环图表示一个工程,其顶点表示活动,用有向边<Vi, Vj>表示活动 Vi 必须先于活动 Vj 进行的这样一种关系,则将这种有向图称为顶点表示活动的网络,简称AOV网。在AOV网中,活动 Vi 是活动 Vj 的直接前驱,Vj 是Vi 的直接后继,这种前驱和后继关系具有传递性,且任何活动 Vi 不能以它自己作为自己的前驱或后继。
2)拓扑排序:在图论中,由一个有向无环图的顶点组成的序列,当且仅当满足下列条件时,称为该图的一个拓扑排序:
① 每个顶点出现且仅出现一次;
② 若顶点A在序列中排在顶点B的前面,则在图中不存在从B到A的路径。
或定义为:拓扑排序是对有向无环图的顶点的一种排序,它使得若存在一条从顶点A到顶点B的路径,则在排序中B出现在A的后面。每个AOV网都有一个或多个拓扑排序序列。
2、对AOV网进行拓扑排序的算法步骤:
① 从AOV网中选择一个没有前驱(入度为0)的顶点并输出;
② 从网中删除该顶点和所有以它为起点的有向边;
③ 重复①和②直到当前的AOV网为空或当前网中不存在无前驱的顶点为止。后一种情况说明有向图中必然存在环。
3、对AOV网进行拓扑排序的示例:
下图为拓扑排序过程的示例。每轮选择一个入度为0的顶点并输出,然后删除该顶点和所有以它为起点的有向边,最后得到的拓扑排序的结果为{1, 2, 4, 3, 5}。
用拓扑排序算法处理AOV网时,需要注意以下几个问题:
1)入度为0的顶点,即没有前驱活动的或前驱活动都已经完成的顶点,工程可以从这个顶点所代表的活动开始或继续。
2)拓扑排序的结果可能不唯一。AOV网的各顶点为线性序列是拓扑排序唯一的充分非必要条件,拓扑排序是否唯一的判断条件是在每次输出顶点时,检测入度为0的顶点是否唯一,若都唯一,则说明拓扑序列唯一。
3)由于AOV网中各顶点的地位平等,每个顶点的编号是人为定的,因此可以按拓扑排序的结果进行重新编号,从而生成AOV网的新的邻接存储矩阵,这个新邻接矩阵可以是三角矩阵。因此对于一般的图来说,若其邻接矩阵是三角矩阵,则存在拓扑序列,反之则不一定成立。
4、拓扑排序的代码实现:
bool ToplogicalSort(Graph G){
InitStack(S); //初始化栈,存储入度为0的顶点
int i;
for(int i=0; i<G.vexnum; i++){
if(indegree[i] == 0){
Push(S, i); //将所有入度为0的顶点进栈
}
}
int count = 0; //count用于记录当前已经输出的顶点数
while(!IsEmpty(S)){ //栈不空则说明还存在入度为0的顶点
Pop(S, i); //栈顶元素出栈
print[count++] = i; //输出栈顶元素,count值+1
for(p=G.vertices[i].firstarc; p; p=p->nextarc){
//将所有i指向的顶点的入度-1,并将入度减为0顶点压入栈S
v = p->adjvex;
if(!(--indegree[v])){
Push(S, v); //入度为0,则入栈
}
}
}
if(count<G.vexnum){ //排序失败,有向图中有回路
return false;
}
else{ //拓扑排序成功
return true;
}
}
5、拓扑排序的时间复杂度:
因为输出每个顶点的同时还要删除以它为起点的边,所以采用邻接表存储时拓扑排序的时间复杂度为O(|V|+|E|),采用邻接矩阵存储时拓扑排序的时间复杂度为O(|V|2)。
6、对AOV网进行逆拓扑排序的算法步骤:
对一个AOV网,若采用下列步骤进行排序,则称之为逆拓扑排序:
① 从AOV网中选择一个没有后继(出度为0)的顶点并输出;
② 从网中删除该顶点和所有以它为终点的有向边;
③ 重复①和②直到当前的AOV网为空。
7、深度优先搜索(DFS)实现拓扑排序的思想和代码实现:
利用深度优先搜索(Depth-First-Search, DFS)遍历也可以实现拓扑排序。对于有向无环图G中的任意结点 u, v,它们之间的关系必然为以下三种形式之一:
1)若 u 是 v 的祖先:则在调用DFS访问 u 之前,必然已经对 v 进行了DFS访问,即 v 的DFS结束时间先于 u 的DFS结束时间(祖先的结束时间必然大于子孙的结束时间)。从而可考虑在DFS函数中设置一个时间标记,在DFS调用结束时,对各顶点计时(即在DFS的基础上加入time变量)。
2)若 u 是 v 的子孙:则 v 是 u 的祖先,v 的结束时间大于 u 的结束时间。
3)若 u 与 v 之间没有路径关系:则 u 和 v 在拓扑序列上的关系任意。
按照结束时间从大到小进行排列,就可以得到一个拓扑排序序列。代码实现如下:
【深度优先遍历和拓扑排序都可以判断出一个有向图是否有环(回路)】
bool visited[MAX_VERTEX_NUM]; //访问标记数组
void DFSTraverse(Graph G){
for(v=0; v<G.vexnum; ++v){ //初始化访问标记数组
visited[v] = FALSE;
}
time = 0;
for(v=0; v<G.vexnum; ++v){ //从v=0开始遍历
if(!visited[v]){ //未被访问则进行深度优先遍历
DFS{G, v};
}
}
}
void DFS(Graph G, int v){
visited[v] = TRUE; //设置已被访问
visit(v);
for(w=FirstNeighbor(G, v); w>=0; w=NextNeighbor(G, v, w)){
if(!visited[w]){ //w为v的尚未访问的邻接点
DFS(G, w);
}
}
time = time+1;
finishTime[v] = time;
}
三、关键路径
1、AOE网和关键路径的概念:
1)AOE网:在带权有向图中,以顶点表示事件、以有向边表示活动、以边上的权值表示完成该活动的开销(如完成活动所需的时间),称之为用边表示活动的网络,简称AOE网。
AOE网和AOV网都是有向无环图,不同之处在于它们的边和顶点所代表的含义是不同的, AOE网中的边有权值;而AOV网中的边无权值,仅表示顶点之间的前后关系。AOE网具有以下两个性质:
① 只有在某顶点所代表的事件发生后,从该顶点出发的各有向边所代表的活动才能开始;
② 只有在进入某顶点的各有向边所代表的活动都已结束时,该顶点所代表的事件才能发生。
在AOE网中仅有一个入度为0的顶点,称为开始顶点(源点),它表示整个工程的开始;网
中也仅存在一个出度为0的顶点,称为结束顶点(汇点),它表示整个工程的结束。
2)关键路径:在AOE 网中,有些活动是可以并行进行的。从源点到汇点的有向路径可能有多条,并且这些路径长度可能不同。完成不同路径上的活动所需的时间虽然不同,但是只有所有路径上的活动都已完成,整个工程才能算结束。因此,从源点到汇点的所有路径中,具有最大路径长度的路径称为关键路径,而把关键路径上的活动称为关键活动。
2、寻找关键路径所用的参量:
完成整个工程的最短时间就是关键路径的长度,即关键路径上各活动花费开销的总和。这是因为关键活动影响了整个工程的时间,即若关键活动不能按时完成,则整个工程的完成时间就会延长。因此,只要找到了关键活动,就找到了关键路径,也就可以得出最短完成时间。
下面给出在寻找关键活动时所用到的几个参量的定义:
1)事件 vk 的最早发生时间 ve(k):
它是指从源点 v1 到顶点 vk 的最长路径长度。事件 vk 的最早发生时间决定了所有从 vk 开始的活动能够开工的最早时间。可用下面的递推公式来计算:
① ve(源点) = 0;
② ve(k) = Max{ ve(j) + Weight(vj, vk) },vk 为 vj 的任意后继,Weight(vj, vk) 表示 <vj, vk> 上的权值。
计算 ve() 值时,按从前往后的顺序进行,可以在拓扑排序的基础上计算:
① 初始时,令 ve[1…n] = 0。
② 输出一个入度为0 的顶点 vj 时,计算它所有直接后继顶点 vk 的最早发生时间,若 ve[j] + Weight(vj, vk) > ve[k],则 ve[k] = ve[j] + Weight(vj, vk)。以此类推,直至输出全部顶点。
2)事件 vk 的最迟发生时间 vl(k):
它是指在不推迟整个工程完成的前提下,即保证它的后继事件 vj 在其最迟发生时间 vl(j) 能够发生时,该事件最迟必须发生的时间。可用下面的递推公式来计算:
① vl(汇点) = ve(汇点);
② vl(k) = Min{ vl(j) - Weight(vk, vj) },vk 为 vj 的任意前驱。
注意:在计算 vl(k) 时,按从后往前的顺序进行,可以在逆拓扑排序的基础上计算。
计算 vl() 值时,按从后往前的顺序进行,在上述拓扑排序中,增设一个栈以记录拓扑序列,拓扑排序结束后从栈顶至栈底便为逆拓扑有序序列。过程如下:
① 初始时,令 vl[1…n] = ve[n] 。
② 栈顶顶点vj 出栈,计算其所有直接前驱顶点 vk 的最迟发生时间,若 vl[j] - Weight(vk, vj) < vl[k],则vl[k] = vl[j] - Weight(vk, vj) 。以此类推,直至输出全部栈中顶点。
3)活动 ai 的最早开始时间 e(i):它是指该活动弧的起点所表示的事件的最早发生时间。若边 <vk, vj> 表示活动 ai,则有 e(i) = ve(k) 。
4)活动 ai 的最迟开始时间 l(i):它是指该活动弧的终点所表示事件的最迟发生时间与该活动所需时间之差。若边 <vk, vj> 表示活动 ai,则有 l(i) = vl(j) - Weight(vk, vj)。
5)一个活动 ai 的最迟开始时间 l(i) 和其最早开始时间 e(i) 的差额 d(i) = l(i) - e(i):
它是指该活动完成的时间余量,即在不增加完成整个工程所需总时间的情况下,活动 ai 可以拖延的时间。若一个活动的时间余量为0,则说明该活动必须要如期完成,否则就会拖延整个工程的进度,所以称 l(i) - e(i) = 0 即 l(i) = e(i) 的活动 ai 是关键活动。
3、寻找AOE网的关键路径的算法步骤:
① 从源点出发,令 ve(源点) = 0,按拓扑有序求其余顶点的最早发生时间 ve();
② 从汇点出发,令 vl(汇点) = ve(汇点),按逆拓扑有序求其余顶点的最迟发生时间 vl()。
③ 根据各顶点的 ve() 值求所有弧的最早开始时间 e()。
④ 根据各顶点的 vl() 值求所有弧的最迟开始时间 l()。
⑤ 求AOE网中所有活动的差额 d(),找出所有 d() = 0 的活动构成关键路径。
4、寻找AOE网的关键路径的示例:
下图为求解关键路径的示例:
下面给出求解关键路径的详细过程:
1)求事件的最早发生时间 ve():【口诀:(事件)从前往后找较大,从后往前找较小。】
图的拓扑排序序列为:v1–>v3–>v2–>v5–>v4–>v6。(该图的拓扑排序不唯一)
初始 ve(1) = 0;
在拓扑排序输出顶点过程中,求得 ve(3) = a2 = 2,ve(2) = a1 = 3,ve(5) = a1 + a4 = 6;
ve(4) = max{ ve(2) + a3, ve(3) + a5 } = max{5, 6} = 6(a1、a3不是关键路径);
ve(6) = max{ ve(5) + a8, ve(4) + a7, ve(3) + a6 } = max{7, 8, 5} = 8(a4、a6、a8不是关键路径)。
可以得到关键路径为:v1–>v3–>v4–>v6。
2)求事件的最迟发生时间 vl():【口诀:(事件)从前往后找较大,从后往前找较小。】
逆拓扑排序序列为:v6–>v4–>v5–>v2–>v3–>v1。(该图的逆拓扑排序不唯一)
初始 vl(6) = 8,显然 vl(1) = 0;
在逆拓扑排序出栈过程中,求得 vl(4) = 8 - a7 = 6,vl(5) = 8 - a8 = 7;
vl(2) = min{ vl(5) - a4, vl(4) - a3 } = min{4, 4} = 4;
vl(3) = min{ vl(4) - a5, vl(6) - a6 } = min{2, 5} = 2。
3)求活动的最早开始时间 e():【口诀:(活动)早头晚尾。】
弧的最早开始时间 e() 等于该弧的起点的顶点的 ve()。
弧 a8 的起点的顶点为 v5,ve(5) = 6;
弧 a7 的起点的顶点为 v4,ve(4) = 6;
弧 a6、a5 的起点的顶点为 v3,ve(3) = 2;
弧 a4、a3 的起点的顶点为 v2,ve(2) = 3;
弧 a2、a1 的起点的顶点为 v1,ve(1) = 0。
4)求活动的最迟开始时间 l():【口诀:(活动)早头晚尾。】
弧的最迟开始时间 e() 等于该弧的终点的顶点的 vl() 减去该弧持续的时间。
弧 a8(=1)、a7(=2)、a6(=3) 的终点的顶点为 v6,vl(6) = 8,再减去它们各自的持续时间;
弧 a4(=3) 的终点的顶点为 v5,vl(5) = 7,再减去 a4 的持续时间;
弧 a5(=4)、a3(=2) 的终点的顶点为 v4,vl(4) = 6,再减去它们各自的持续时间;
弧 a2(=2) 的终点的顶点为 v3,vl(3) = 2,再减去 a2 的持续时间;
弧 a1(=3) 的终点的顶点为 v2,vl(2) = 4,再减去 a1 的持续时间。
5)求活动的最迟开始时间和其最早开始时间的差额 d() = l() - e():
根据 l(i) - e(i) = 0 的关键活动,得到的关键路径为 (v1, v3, v4, v6)。
5、缩短工期的相关分析:
对于关键路径,需要注意以下几点:
1)关键路径上的所有活动都是关键活动,它是决定整个工程的关键因素,因此可以通过加快关键活动来缩短整个工程的工期。但也不能任意缩短关键活动,因为一旦缩短到一定的程度,该关键活动就可能会变成非关键活动。
2)网中的关键路径并不唯一,且对于有几条关键路径的网,只提高一条关键路径上的关键活动速度并不能缩短整个工程的工期,只有加快那些包括在所有关键路径上的关键活动才能达到缩短工期的目的。
采用不同存储结构时DFS算法、拓扑排序和关键路径的时间复杂度如下图所示:
(其中n为顶点数,e为边数)