关于有向无环图和拓扑排序(摘自算法基础)

来源:互联网 发布:快递怎么找淘宝客户 编辑:程序博客网 时间:2024/06/08 11:51

有向图:

由定点和有向边组成。

每个有向边是一个形如(u,v)的有序对,其中u和v代表顶点。

当一个有向图包含一个有向边(u,v)时,我们称v临接于u,并且(u,v)表示从u发出,且进入v。

有向无环图:不存在任何一个从某个顶点发出,经过一条或者多条边后,重新又回到出发点的路径。

入度:进入该顶点的边的个数称为该顶点的入度。(可以以任意一个入度为0的顶点作为起始顶点。)

出度:从该顶点发出的边的个数。

任何一个有向无环图中必定至少存在一个入度为0的顶点,至少存在一个出度为0的顶点,否则图中必存在环。

有向无环图对于构造一个任务必须发生在另一个任务之前的这种依赖模型特别有效。有向无环图的另一个应用是规划项目,例如建造房屋时,框架的设计必须在盖屋顶之前。

如何表示有向图?

在计算机中,可以用若干种方式来表示一个有向图,若一个图包含n个顶点,m条边,同时假定每个顶点的编号是介于1~n的范围之内,则能将顶点看作是数组索引,或者甚至是看成是一个矩阵的行号或者列号。

A.如果仅仅想要知道存在哪些顶点和哪些边,则使用一个n*n的邻接矩阵来表示一个图,其中每行和每列均对应着一个顶点,并且顶点u所对应的行和顶点v所对应的列的交叉位置处,当边(u,v)存在时,该位置处为1,若图中不包含边(u,v),该交叉位置处为0。由于一个邻接矩阵包含n^2个项,那么m<=n^2必定是成立的。

B.另一种表示方式是可以只保存一个具有m条边的无序列表。将邻接矩阵和无序列表结合起来就得到了一种新的图的表示方式——邻接表,即一个n元素数组,索引是各个顶点,且每个顶点u所对应的数组项是顶点u的所有邻接顶点所组成的表。总而言之,邻接表的所有顶点所对应的数组项中共包含m个顶点

然而边的无序列表和邻接表会产生一个问题:如何表示一个表。表示一个表的最好方法取决于我们需要在表上完成什么类型的操作。

对于边的无序列表和邻接表,我们已经提前知道了每个表中边的个数,且表的内容不会发生改变,因此我们能将每个表存储在一个数组中。即使表的内容会随着时间的变化而发生改变,只要我们知道在任意时刻一个表中所包含的最大个数,我们也可以使用数组来存储该表。如果不需要在表的内部插入新项或者删除项,那么使用数组来表示表的效率和其他方式一样高。

如果确实需要在表中插入新项,那么我们可以使用链表,链表中的每项君包含它的后继位置,如果需要在表内删除元素,那么链表也应该包括该项的前驱位置,以便我们能迅速地建立新的次序关系。

单向链表:仅仅包括后继链的链表

双向链表:不仅包括后继链,还包含前驱链的链表



拓扑排序:

一个有向无环图的拓扑排序需要产生一个线性序列:如果(u,v)是有向无环图的一条边,那么在线性序列汇中,u必须出现在v之前。

程序 TOPOLLOGICAL-SORT(G)

输入 G:一个顶点编号为从1~n的有向无环图

输出 关于顶点的一个线性序列(例如:如果(u,v)是图上的一条边,那么在线性序列中,u就出现在v之前)

步骤

1.令in-degree[1......n]为一个新数组,创建一个空的关于顶点的线性序列。

2.令in-degree数组中的每个元素均为0。

3.对于每个顶点u:

A.对于每个与顶点u相邻接的顶点v:

i.增加in-degree[v]的值。

4.创建一个列表next,用以存放所有满足in-degree[u]=0的顶点u。

5.只要next列表不为空,执行如下操作:

A.从next列表中删除一个顶点(将该顶点称为顶点u)。

B.将u添加到线性序列的末尾处。

C.对于每个与u相邻接的顶点v:

i.令in-degree[v]的值自减一。

ii.如果in-degree[v]=0,将顶点v添加到next列表中。

6.返回线性序列。

拓扑排序程序中仅仅记录了每个顶点的入度,并将理论上移除的边所指向的顶点的入度减一。由于数组索引是整数,现假定每个顶点均由一个介于1~n范围内的一个确定的整数表示。因为该程序需要快速地确定入度为0的顶点,它将每隔顶点的入读存储在数组in-degree中,并且将所有入度为0的顶点存储在列表next中。第1~3步初始化in-degree数组,第4步初始化next列表,且当顶点和边在概念上被移除时,第5步负责更新in-degree数组和next列表。该程序能选择next列表中的任何一个顶点作为线性序列的下一个元素。

拓扑排序的运行时间:

假定有向无环图使用邻接表表示并且next表是一个链表,那么可证TOPOLOGICAL-SOFT程序所花费的时间是ø(n+m)。


PERT图表中的关键路径:

<PERT:"program evaluation and review technique",即令多个任务尽可能地同时执行,完成整个工作的时间被称为PERT图表的“关键路径”>

<路径:指一个顶点和边构成的序列,该序列允许从一个顶点到达另一个顶点(允许从一个顶点到达它本身)的边的序列>

<环:一条从一个顶点出发,最后又能回归原顶点的路径>

<权:用来表示和边相关联的一个通用术语。一个边上带有权重的有向图称为加权有向图>

<一条路径的权重:表示路径上所有边的权重之和>

<最短路径:表示从顶点u到顶点v的所有路径中的边的权重和最小的路径,最短路径并不一定是唯一的,因为一个有向图中从顶点u到顶点v可能存在多条最短路径>


一个PERT图表中的一条关键路径是指所有路径中完成任务所花费时间总和最大的路径。沿着一条关键路径完成任务所花费的时间给出了无论多少个任务被同时执行,完成整个工作所需要花费的最少可能时间。

为了将任务时间取反的PERT图表转化为一个加权有向图,将每个顶点上的取反的任务时间转化为所有指向它的边上的任务时间。也就是说,如果顶点v有一个(非-取反的)任务时间t,那么将每个满足(u,v)的边,即指向v的边均设置为-t。

//在得出关键路径之前,需要先学会求最短路径。

有向无环图中的最短路径:

我们假定有向无环图被存储在一个邻接表中,并且将于边(u,v)关联的权重存储为weight(u,v)。

在一个由PERT图表所衍生出来的有向无环图中,想要寻找一条从源点汇点的最短路径(将源点称为start,将汇点称为finish)

将源点命名为s,并且计算出关于每个顶点的两个值。首先,从源点s到顶点v的最短路径,用sp(s,v)表示,接着从源点s到顶点v的最短路径中的顶点v的前驱:顶点v的前驱是满足以下条件的顶点u:从源点s到顶点v的最短路径等于从源点s到顶点u的最短路径再加上边(u,v)的权重。我们对n个顶点从1到n进行编号,以方便执行最短路径算法。

将最短路径算法的结果分别存储在数组shortest[1......n]和数组pred[1......n]中。(运行过程中两个数组中的值可能不是最终的正确值,可是当算法执行完毕之后,两个数组存储的结果就是正确的值)

我们需要处理所产生的几个问题。

1.要是从顶点s到顶点v不存在路径呢?

那么我们就设sp(s,v)=∞,则shortest[v]结果应该是∞。因为顶点v在从源点s出发的最短路径上不存在前驱,所以pred[v]应该为NULL。而顶点s也没有前驱,所以称pred[s]也应该为NULL。

2.图中可能存在环,同时也存在带负权重的边,要是存在一个权重和为负的环呢?

那么我们可以循环地无穷次在该环上执行操作,每循环一次就会使得路径上的权重降低一些。然而,我们仅仅关心无环图,所以图中不会存在环路,我们也无需担心权重和为负的环了。

为了计算从源点s出发的最短路径,我们以shortest[s]=0开始计算,对于所有其他顶点v,shortest[v]=∞(因为我们提前并不知道从顶点s出发时,我们能够到达哪个顶点),并且对于所有的顶点v,均有pred[v]=NULL成立。随后我们对图上的边应用一系列的松弛步骤

程序 RELAX(u,v)

输入 u,v:边(u,v)的顶点u,v。

结果 shortest[v]的值可能会减小,如果它确实会减小,那么令pred[v]取u。

步骤

1.如果shortest[u]+weight(u,v)<shortest[v],那么将shortest[v]赋值为shortest[u]+weight(u,v),将pred[v]赋值为u。

当调用RELAX(u,v)时,我们判定能否通过将(u,v)作为最后一条边来改进从源点s到顶点v的最短路径。

如果沿着最短路径按序对各个边执行松弛操作,我们会得到正确的结果。(可是,当我们甚至都还不知道哪条路径是最短路径的时候,如何能做到沿着最短路径按序对各个边执行松弛操作呢?A:对于一个有向无环图来说,操作很简单,我们对有向无环图中的所有边进行松弛操作,并且当我们对于所有边执行松弛操作时,每条最短路径上的边也都按序被执行了松弛操作。)

下面是关于如何沿着最短路径上的边进行松弛操作的更精准的描述,并且它适用于任何有向图(无论是否存在环):

对于除源点之外的所有顶点,shortest[u]=∞,对于所有顶点pred[u]=NULL,而对于源点s,shortest[s]=0。

随后对从源点s到任何顶点v的最短路径上的边执行松弛操作(以从源点s出发的边开始,并且一直到进入顶点v的边结束)。对于其他边的松弛操作可能会大量地穿插在沿着这个最短路径进行松弛操作的过程中,但是只有松弛操作才可能会改变shortest或者pred的值。

当对边执行松弛操作后,顶点v的shortest值和pred值是正确的:shortest[v]=sp(u,v),且pred[v]是位于从源点s出发的最短路径上的顶点v的前驱。

由上,我们可以沿着每条最短路径按序精确地对每条边执行松弛操作。

首先,利用拓扑排序对有向无环图进行排序。

随后,对于按照拓扑排序后所得到的线性序列中的每个顶点,依序对从每个顶点出发的所有边执行松弛操作。

由于每条边必定是从线性序列中的排列在较前侧的顶点出发,然后进入到线性序列中排列在后面的顶点,因此有向无环图中的每个路径一定会以一种与线性序列相一致的顺序来访问各个顶点。

程序 DAG-SHORTEST-PATHS(G,s)

输入 G:一个加权有向无环图(包含具有n个顶点的集合V,m条有向边的集合E)

s:集合V中的一个源点

结果 对于集合V中的每个非源顶点v,shortest[v]表示从s 到v的一条最短路径的权重和sp(s,v),pred[v]表示在这条最短路径上出现在顶点v之前的顶点。对于源点s,shortest[s]=0,pred[s]=NULL。如果从s到v没有路径,那么shortest[v]为∞,pred[v]=NULL。

步骤

1.调用TOPOLOGICAL-SORT(G),L被赋值为由TOPOLOGICAL-SORT(G)调用所返回的顶点的线性序列。

2.对于除了顶点s之外的任一顶点v,shortest[v]均被赋值为∞,将shortest[s]赋值为0,对于每个顶点v,将pred[v]赋值为NULL。

3.按照线性序列L的排序,依次取线性序列L中的顶点为u:

A.对于每个与顶点u相邻接的顶点v:

i.调用RELAX(u,v)。



0 0