算法导论-上课笔记9:基本的图算法


0 前言

本文将介绍图的表示和图的搜索。图的搜索指的是系统化地跟随图中的边来访问图中的每个结点。图搜索算法可以用来发现图的结构。许多的图算法在一开始都会先通过搜索来获得图的结构,其他的一些图算法则是对基本的搜索加以优化。

图的搜索技巧是整个图算法领域的核心


1 图的表示

对于图G=(V,E),有两种标准表示方法:

1、邻接链表法;

2、邻接矩阵法。

上述两种表示方法都既可以表示无向图,也可以表示有向图。邻接链表因为在表示稀疏图(即边的条数|E|远远小于|V|2的图)时非常紧凑而成为通常的选择。本文给出的多数图算法都假定作为输入的图是以邻接链表方式进行表示的。在稠密图(即|E|接近|V|2的图)的情况下,可能倾向于使用邻接矩阵表示法。另外,如果需要快速判断任意两个结点之间是否有边相连,可能也需要使用邻接矩阵表示法。

1.1 邻接链表法

对于图G=(V,E)来说,其邻接链表表示由一个包含|V|条链表的数组Adj所构成,每个结点有一条链表。对于每个结点u∈V,邻接链表Adj[u]包含所有与结点u之间有边相连的结点v,即Adj[u]包含图G中所有与u邻接的结点,该链表里包含指向这些结点的指针。由于邻接链表代表的是图的边,在伪代码里,将数组Adj看做是图的一个属性,就如将边集合E看做是图的属性一样。因此,在伪代码里,将看到G.Adj[u]这样的表示。下图(记为图1)的a给出的是一个无向图,图1的b给出的是图1的a的邻接链表表示:
在这里插入图片描述
类似地,下图(记为图2)的b给出的是图2的a的有向图的邻接链表表示:
在这里插入图片描述
如果G是一个有向图,则对于边(u,v)来说,结点v将出现在链表Adj[u]里,因此,所有邻接链表的长度之和等于|E|。如果G是一个无向图,则对于边(u,v)来说,结点v将出现在链表Adj[u]里,结点u将出现在链表Adj[v]里,因此,所有邻接链表的长度之和等于2|E|。但不管是有向图还是无向图,邻接链表表示法的存储空间需求均为Θ(V+E)。

对邻接链表稍加修改,即可以用来表示权重图。权重图是图中的每条边都带有一个相关的权重的图。该权重值通常由一个w:E→R的权重函数给出。例如,设G=(V,E)为一个权重图,其权重函数为w,可以直接将边(u,v)∈E的权重值w(u,v)存放在结点u的邻接链表里。从这种意义上说,邻接链表表示法的鲁棒性很高,可以对其进行简单修改来支持许多其他的图变种。

邻接链表的一个潜在缺陷是无法快速判断一条边(u,v)是否是图中的一条边,唯一的办法是在邻接链表Adj[u]里面搜索结点v。邻接矩阵表示则克服了这个缺陷,但付出的代价是更大的存储空间消耗。

1.2 邻接矩阵法

对于邻接矩阵表示来说,通常会将图G中的结点编为1,2,…,|V|,这种编号可以是任意的。在进行此种编号之后,图G的邻接矩阵表示由一个|V|×|V|的矩阵A=(aij)予以表示,该矩阵满足下述条件:
在这里插入图片描述
【1.1 邻接链表法】一节的图1的c和图2的c分别给出的是图1的a的无向图和图2的a的有向图的邻接矩阵表示。

不管一个图有多少条边,邻接矩阵的空间需求皆为Θ(V2)。

从【1.1 邻接链表法】一节的图1的c可以看到,无向图的邻接矩阵是一个对称矩阵。由于在无向图中,边(u,v)和边(v,u)是同一条边,无向图的邻接矩阵A就是自己的转置,即A=AT。在某些应用中,可能只需要存放对角线及其以上的这部分邻接矩阵,从而将图存储空间需求减少几乎一半。

与邻接链表表示法一样,邻接矩阵也可以用来表示权重图。例如,如果G=(V,E)为一个权重图,其权重函数为w,则直接将边(u,v)∈E的权重w(u,v)存放在邻接矩阵中的第u行第v列记录上。对于不存在的边,则在相应的行列记录上用0或者∞来表示。

与邻接链表表示法相比,邻接矩阵表示法更为简单,因此在图规模比较小时,可能更倾向于使用邻接矩阵表示法。而且,对于无向图来说,邻接矩阵还有一个优势:每个记录项只需要1位的空间。

比较项二者哪个更优
检验边(u,v)是否存在邻接矩阵法
确定顶点u的度邻接链表法
存储稀疏图邻接链表法
存储稠密图邻接矩阵法
边的插入或删除操作邻接矩阵法
图的遍历邻接链表法
大多数问题邻接链表法

1.3 表示图的属性

对图进行操作的多数算法需要维持图中结点或边的某些属性。这些属性可以使用通常的表述法来进行表示,如v.d表示结点v的属性d。当使用一对结点来表示一条边的时候,也可以使用同样风格的表述。例如,如果边有一种属性f,则边(u,v)的这种属性可以表示为(u,v).f。对于表示和理解算法而言,这种属性表述足够清晰。

不过,在算法的实际程序里面实现结点和边的属性则完全是另外一回事情。没有什么最好的办法来存放和访问结点与边的属性。对于给定的场景,所做出的决定可能依赖于诸多因素:所使用的程序设计语言、需要实现的算法和程序中使用图的方式等因素。如果使用邻接链表来表示图,一种可能的方法是使用额外的数组来表示结点属性,如一个与Adj数组相对应的数组d[1…|V|]。如果与u邻接的结点都在Adj[u]中,则属性u.d将存放在数组项d[u]里。还有许多其他方法可以用来实现属性的表示。例如在面向对象的程序设计语言里,结点属性可以表示为Vertex类的一个子类中的实例变量。


2 广度优先搜索

广度优先搜索是图的搜索算法之一,也是许多重要的图算法的原型,Prim的最小生成树算法和Dijkstra的单源最短路径算法都使用了广度优先搜索的思想。

给定图G=(V,E)和一个可以识别的源结点s,广度优先搜索对图G中的边进行系统性的探索来发现可以从源结点s到达的所有结点。该算法能够计算从源结点s到每个可到达的结点的以最少的边数的可达距离,同时生成一棵“广度优先搜索树”。该树以源结点s为根结点,包含所有可以从s到达的结点。对于每个从源结点s可以到达的结点v,在广度优先搜索树里从结点s到结点v的简单路径所对应的就是图G中从结点s到结点v的“最短路径”,即包含最少边数的路径。该算法既可以用于有向图,也可以用于无向图。

广度优先搜索之所以如此得名是因为该算法始终是将已发现结点和未发现结点之间的边界,沿其广度方向向外扩展。也就是说,算法需要在发现所有距离源结点s为k的所有结点之后,才会发现距离源结点s为k+1的其他结点。

为了跟踪算法的进展,广度优先搜索在概念上将每个结点涂上白色、灰色或黑色。所有结点在一开始的时候均涂上白色。在算法推进过程中,这些结点可能会变成灰色或者黑色。在搜索过程中,第一次遇到一个结点就称该结点被“发现”,此时该结点的颜色将发生改变。因此,凡是灰色和黑色的结点都是已被发现的结点。但广度优先搜索对灰色和黑色结点加以区别,以确保搜索按照广度优先模式进行推进。如果边(u,v)∈E且结点u是黑色,则结点v既可能是灰色也可能是黑色。也就是说,所有与黑色结点邻接的结点都已经被发现。对于灰色结点来说,其邻接结点中可能存在未被发现的白色结点。灰色结点所代表的就是已知和未知两个集合之间的边界。

在执行广度优先搜索的过程中将构造出一棵广度优先树。一开始,该树仅含有根结点,就是源结点s。在扫描已发现结点u的邻接链表时,每当发现一个白色结点v,就将结点v和边(u,v)同时加入该棵树中。在广度优先树中,称结点u是结点v的前驱或者父结点。由于每个结点最多被发现一次,因此它最多只有一个父结点。广度优先树中的祖先和后代关系皆以相对于根结点s的位置来进行定义:如果结点u是从根结点s到结点v的简单路径上的一个结点,则结点u是结点v的祖先,结点v是结点u的后代。

在下面给出的广度优先搜索过程BFS中,假定输入图G=(V,E)是以邻接链表所表示的。该算法为图中每个结点赋予了一些额外的属性:将每个结点u的颜色存放在属性u.color里,将u的前驱结点存放在属性u.π里。如果u没有前驱结点(例如,如果u=s或者结点u尚未被发现),则u.π=NIL。属性u.d记录的是广度优先搜索算法所计算出的从源结点s到结点u之间的距离。该算法使用一个先进先出的队列Q来管理灰色结点集。

BFS(G,s)
    for each vertex u∈G.V-{s}
        u.color=WHITE
        u.d=∞
        u.π=NIL
    s.color=GRAY
    s.d=0
    s.π=NIL
    Q=ENQUEUE(Q,s)
    while Q≠∅
        u=DEQUEUE(Q)
        for each v∈G.Adj[u]
            if v.color==WHITE
                v.color=GRAY
                v.d=u.d+1
                v.π=u
                ENQUEUE(Q,v)
        u.color=BLACK

在这里插入图片描述
上图是BFS在无向图上的运行过程。添加了阴影的边是被BFS发现的边。每个结点u里面记录的是u.d的值。图中还给出了在算法第11-19行的while循环每次开始时的队列Q的内容。结点距离标记在了队列相应结点的下方。

BFS的工作过程是这样的,除了源结点s以外,算法的第2-5行将所有结点涂上白色,将每个结点u的u.d属性设置为∞,将每个结点的父结点设置为NIL。第6行将源结点s涂上灰色,因为该结点在算法开始时已被发现。第7行将s.d初始化为0,第8行将源结点s的前驱设置为NIL。第9-10行对队列Q进行初始化,该队列的初始状态仅包含源结点s。

算法第11-19行的while循环一直执行到图中不再有灰色结点时结束,灰色结点指的是已被发现的结点,但其邻接链表尚未被完全检查。该while循环的循环不变量如下:

在算法第11行的测试中,队列Q里面包含的是灰色结点集合。

上述循环不变量在第1次循环前成立,且每次循环过程都维持该不变量的成立。在第1次循环开始之前,唯一的灰色结点,也是队列Q里面的唯一结点,是源结点s。算法第12行取出队列Q的队头结点u,将其从队列中删除。第13-18行的for循环对结点u的邻接链表中的每个结点v进行考察。如果结点v是白色的,则该结点尚未被发现,算法执行第15-18行的程序来发现该结点:算法将结点v涂上灰色,将其距离v.d设置为u.d+1,并且将结点u记录为结点v的父结点v.π,将其插入队列Q的末尾。算法在检查完结点u的邻接链表里的所有结点后将u涂上黑色(第19行)。由于一个结点在涂上灰色(第15行)的同时被加入队列Q中(第18行),而结点在从队列里删除(第12行)的同时被涂上黑色(第19行),所以前面给出的循环不变量一直得到保持。

广度优先搜索的结果依赖于对每个结点的邻接结点的访问顺序(第13行),这使得广度优先树可能会不一样,但BFS算法所计算出来的距离d都是一样的。

下面分析该算法的运行时间,将使用聚合分析(此博文的【1 聚集分析】一节)。在初始化操作结束后,广度优先搜索不会再给任何结点涂上白色,因此,第14行的测试可以确保每个结点的入队次数最多为一次,因而出队最多一次。入队和出队的时间均为O(1),因此,对队列进行操作的总时间为O(V)。因为算法只在一个结点出队的时候才对该结点的邻接链表进行扫描,所以每个邻接链表最多只扫描一次。由于所有邻接链表的长度之和是Θ(E),用于扫描邻接链表的总时间为O(E)。初始化操作的成本是O(V),因此广度优先搜索的总运行时间为O(V+E)。因此,广度优先搜索的运行时间是图G的邻接链表大小的一个线性函数

2.1 最短路径

广度优先搜索能够找出从给定源结点s∈V到所有可以到达的结点之间的距离。定义从源结点s到结点v的最短路径距离δ(s,v)为从结点s到结点v之间所有路径里面最少的边数。如果从结点s到结点v之间没有路径,则δ(s,v)=∞。称从结点s到结点v的长度为δ(s,v)的路径为s到v的最短路径。

引理1:给定G=(V,E),G为一个有向图或无向图,设s∈V为任意结点,则对于任意边(u,v)∈E,δ(s,v)≤δ(s,u)+1。

证明:若结点u是可以从源结点s到达的结点,则v也是从s可以到达的结点。在这种情况下,从源结点s到结点v的最短路径距离不可能比从s到u的最短路径距离加上边(u,v)更长,因此,上述不等式成立。如果结点u不能从s到达,则δ(s,u)=∞,不等式显然成立。

引理1得证。

下面证明BFS能正确计算出每个结点v∈V的v.d=δ(s,v)。首先证明v.d是δ(s,v)的一个上界。

引理2:设G=(V,E)为一个有向图或无向图,假定BFS以给定结点s∈V作为源结点在图G上运行。那么在BFS终结时,对于每个结点v∈V,BFS所计算出的v.d满足v.d≥δ(s,v)。

证明:通过对算法里面ENQUEUE操作的次数进行归纳来证明本引理。做出的归纳假设是:对于所有的结点v∈V,v.d≥δ(s,v)。归纳的基础是BFS在第10行将源结点s加入队列Q后的场景。此时,因为s.d=0=δ(s,s),并且对于所有的结点v∈V-{s},v.d=∞≥δ(s,v),所以归纳假设成立。

对于归纳步,考虑从结点u进行邻接链表搜索时所发现的白色结点v。根据归纳假设,有u.d≥δ(s,u)。从算法第16行的赋值操作和引理1可知:

v.d=u.d+1≥δ(s,u)+1≥δ(s,v)

结点v在这之后被加入到队列Q里,并且因为v在入队时涂上灰色而不会再次入队,因此,第15-18行的子句仅在白色结点上执行。所以,v.d的值不再会发生变化,同样地,归纳假设成立。

引理2得证。

要证明v.d=δ(s,v),首先需要更加精确地研究队列Q在BFS过程中是如何操作的。下面的引理将证明在任意时刻,队列里面最多包含两个不同的d值。

引理3:假定BFS在图G=(V,E)上运行的过程中,队列Q包含的结点为<v1,v2,…,vr>,这里v1是队列Q的头,vr是队列Q的尾。那么vr.d≤v1.d+1,并且对于i=1,2,…,r-1,有vi.d≤vi+1.d。

证明:仍然通过对算法里面入队操作的次数进行归纳来证明本引理。在初始情况下,队列Q里仅包含源结点s,引理直接成立。

对于归纳步,必须证明在入队和出队操作时,引理都成立:

1、出队:如果头结点v1被删除,v2将变为队列里新的头结点(如果队列在删除头结点后为空,则引理直接成立)。根据归纳假设,有v1.d≤v2.d。但仍有vr.d≤v1.d+1≤v2.d+1,且余下的不等式不受影响,即对于i=2,3,…,r-1,有vi.d≤vi+1.d。因此,在v2为头结点时引理依然成立。

2、入队:在算法的第18行将结点v加入队列Q时,该结点成为结点vr+1。在这个时候,已经删除了结点u,并正在对该结点的邻接链表进行检查。根据归纳假设,新的头结点v1满足v1.d≥u.d。因此,vr+1.d=v.d=u.d+1≤v1.d+1。根据归纳假设,有vr.d≤u.d+1,因此,vr.d≤u.d+1=v.d=vr+1.d,余下的不等式不受影响,即对于i=1,2,…,r-1,有vi.d≤vi+1.d。因此,当结点v加入队列时引理仍然成立。

引理3得证。

下面的推论表明,在结点加入到队列时,d值随时间推移单调增长。

推论4:假定在执行BFS时,结点vi和结点vj都加入到队列Q里,并且vi在vj前面入队,则在vj入队时,有vi.d≤vj.d。

证明:根据引理3,以及每个结点获得的d值都是有限的且BFS过程中最多只取一次d值的性质,可以立即得到推论4。

推论4得证。

下面可以证明广度优先搜索算法能够正确计算出最短路径距离了。

定理5:(广度优先搜索的正确性)设G=(V,E)为一个有向图或无向图,又假设BFS以s为源结点在图G上运行。那么在算法执行过程中,BFS将发现从源结点s可以到达的所有结点v∈V,并在算法终止时,对于所有的v∈V,v.d=δ(s,v)。而且,对于任意可以从s到达的结点v≠s,从源结点s到结点v的其中一条最短路径为从结点s到结点v.π的最短路径再加上边(v.π,v)。

证明:使用反证法来证明本定理,假定某些结点获取的d值并不等于其最短路径距离。设v为这样一个结点,则其最短路径距离为δ(s,v),而其所取得的d值不等于该数值,显然v≠s。根据引理2,v.d≥δ(s,v),因此有v.d>δ(s,v)。另外,结点v必定是从s可以到达的,否则将出现δ(s,v)=∞≥v.d。设u为从源结点s到结点v的最短路径上结点v的直接前驱结点,则δ(s,v)=δ(s,u)+1。因为δ(s,u)<δ(s,v),有u.d=δ(s,u)。将这些式子合并起来有:
在这里插入图片描述
将上图记为不等式(1)。

现在来考虑BFS选择将结点v从队列Q里取出的时间(第12行),此时结点v可以是任何颜色。将要证明的是,在每种情况下(即不管v是何种颜色的结点),都将导出与不等式(1)矛盾的情形:

1、如果v是白色结点,则算法的第16行将设置v.d=u.d+1,这与不等式(1)矛盾。

2、如果v是黑色结点,则该结点已经从队列里删除,根据推论4,有v.d≤u.d,再次与不等式(1)矛盾。

3、如果v是灰色结点,则v是在某个结点w出队时被涂上灰色的,而结点w在结点u之前出队(本句需要思考一番),并且v.d=w.d+1。根据推论4,w.d≤u.d,因此有v.d=w.d+1≤u.d+1,这再次与不等式(1)相矛盾。

因此对于所有的v∈V,v.d=δ(s,v)。所有从s可以到达的结点v都必定被发现,否则将有∞=v.d>δ(s,v)。而要获得最终的结论,只要注意到如果v.π=u,则v.d=u.d+1。因此,通过将从源结点s到结点v.π的最短路径加上边(v.π,v),即可以获得从源结点s到结点v的最短路径。

定理5得证。

2.2 广度优先树

过程BFS在对图进行搜索的过程中将创建一棵广度优先树,该棵树对应的是π属性。对于图G=(V,E)和源结点s,定义图G的前驱子图为Gπ=(Vπ,Eπ),其中Vπ={v∈V:v.π≠NIL}∪{s},Eπ={(v.π,v):v∈Vπ-{s}}。

如果Vπ由从源结点s可以到达的结点组成,并且对于所有的v∈Vπ,子图Gπ包含一条从源结点s到结点v的唯一简单路径,且该路径也是图G里面从源结点s到结点v之间的一条最短路径,则前驱子图Gπ是一棵广度优先树。广度优先树实际上就是一棵树,因为它是连通的,并且|Eπ|=|Vπ|-1。称Eπ中的边为树边

下面的引理表明BFS过程所生成的前驱子图是一棵广度优先树。

引理6:当运行在一个有向或无向图G=(V,E)上时,BFS过程所建造出来的π属性使得前驱子图Gπ=(Vπ,Eπ)成为一棵广度优先树。

证明:BFS在第17行设置v.π=u当且仅当(u,v)∈E并且δ(s,v)<∞,即如果结点v可以从源结点s到达,Vπ由从源结点s可以到达的V集合里面的结点所组成。由于Gπ形成一棵树,根据:
在这里插入图片描述
可知Gπ包含从源结点s到Vπ集合里每个结点的一条唯一简单路径。通过递归应用定理5,可以获得每条这样的路径也是图G里面的一条最短路径。因此Gπ是一棵广度优先树。

引理6得证。

下面的伪代码将打印出从源结点s到结点v的一条最短路径上的所有结点,这里假定BFS已经计算出一棵广度优先树:

PRINT-PATH(G,s,v)
    if v==s
        print s
    else if v.π==NIL
        print "no path from" s "to" v "exists"
    else PRINT-PATH(G,s,v.π)
        print v

因为每次递归调用时的路径都比前一次调用中的路径少一个结点,所以该过程的运行时间是关于输出路径上顶点总数的一个线性函数。


3 深度优先搜索

深度优先搜索总是对最近才发现的结点v的出发边进行探索,直到该结点的所有出发边都被发现为止。一旦结点v的所有出发边都被发现,搜索则“回溯”到v的前驱结点(v是经过该结点才被发现的),来搜索该前驱结点的出发边。该过程一直持续到从源结点可以达到的所有结点都被发现为止。如果还存在尚未发现的结点,则深度优先搜索将从这些未被发现的结点中任选一个作为新的源结点,并重复同样的搜索过程。该算法重复整个过程,直到图中的所有结点都被发现为止。

像广度优先搜索一样,在对已被发现的结点u的邻接链表进行扫描时,每当发现一个结点v时,深度优先搜索算法将对这个事件进行记录,将v的前驱属性v.π设置为u。不过,与广度优先搜索不同的是,广度优先搜索的前驱子图形成一棵树,而深度优先搜索的前驱子图可能由多棵树组成,因为搜索可能从多个源结点重复进行。因此,给深度优先搜索的前驱子图所下的定义与对广度优先搜索前驱子图所下的定义略有不同:设图Gπ=(V,Eπ),其中Eπ={(v.π,v):v∈V且v.π≠NIL}。深度优先搜索的前驱子图形成一个由多棵深度优先树构成的深度优先森林。森林Eπ中的边仍然称为树边。

像广度优先搜索算法一样,深度优先搜索算法在搜索过程中也是对结点进行涂色来指明结点的状态。每个结点的初始颜色都是白色,在结点被发现后变为灰色,在其邻接链表被扫描完成后变为黑色。该方法可以保证每个结点仅在一棵深度优先树中出现,因此,所有的深度优先树是不相交的(disjoint)。

除了创建一个深度优先搜索森林外,深度优先搜索算法还为每个结点v盖上了两个时间戳:

1、第一个时间戳v.d记录结点v第一次被发现(discover)的时间(涂上灰色的时候);

2、第二个时间戳v.f记录的是搜索完成(finish)对v的邻接链表扫描的时间(涂上黑色的时候)。

上述两个时间戳提供了图结构的重要信息,通常能够帮助推断深度优先搜索算法的行为。

下面的深度优先搜索算法的伪代码将其发现结点u的时刻记录在属性u.d中,将其完成对结点u处理的时刻记录在属性u.f中。因为|V|个结点中每个结点只能有一个发现事件和一个完成事件,所以这些时间戳都是处于1和2|V|之间的整数。对于每个结点u,有:u.d<u.f。而且结点u在时刻u.d之前为白色,在时刻u.d和u.f之间为灰色,在时刻u.f之后为黑色。

下面的伪代码给出的是基本的深度优先搜索算法DFS,其中输入图G既可以是无向图,也可以是有向图;变量time是一个全局变量,用来计算时间戳。

DFS(G)
    for each vertex u∈G.V
        u.color=WHITE
        u.π=NIL
    time=0
    for each vertex u∈G.V
        if u.color==WHITE
            DFS-VISIT(G,u)

下面的伪代码给出的是DFS-VISIT:

DFS-VISIT(G,u)
    time+=1
    u.d=time
    u.color=GRAY
    for each v∈G.Adj[u]
        if v.color==WHITE
            v.π=u
            DFS-VISIT(G,v)
    u.color=BLACK
    time+=1
    u.f=time

下图(记为图4)描述的是深度优先搜索算法DFS在【1.1 邻接链表法】一节的图2上运行的过程:
在这里插入图片描述
在上图中,随着算法对边的探索的推进,这些边或者变成有阴影的边(如果它们是树边),或者变为虚线边(其他情况)。非树边则根据其为后向(back)边、横向(cross)边或前向(forward)边而分别标记为B、C或F。在【3.2 边的分类】一节中,会详细介绍树边、后向边、横向边和前向边的详细概念。除此之外,结点中的时间戳表明该结点的发现时间和完成时间。

DFS的运行过程如下:

1、第2-4行将所有的结点涂成白色,将所有结点的π属性设置为NIL。

2、第5行将全局时间计数器进行复位。

3、第6-8行依次对每个结点进行检查。当一个白色结点被发现时,则使用DFS-VISIT对结点进行访问。每次在算法第7行调用DFS-VISIT(G,u)时,结点u便成为深度优先森林中一棵新的深度优先树的根结点。当DFS算法返回时,每个结点u都已经被赋予一个发现时间u.d和一个完成时间u.f。

在每次对DFS-VISIT(G,u)的调用中,结点u的初始颜色都是白色。算法的第2行将全局变量time的值进行递增,第3行将time的新值记录为发现时间u.d,第4行将结点u涂上灰色。第5-8行对结点u的每个邻接结点v进行检查,并在v是白色的情况下递归访问结点v。随着每个结点v∈Adj[u]在第5行被考虑,则称深度优先搜索算法已经探索了边(u,v)。最后,在从结点u发出的每条边都被探索后,算法的第9-11行将结点u涂上黑色,对变量time的值进行递增,并将完成时间记录在属性u.f中。

深度优先搜索的结果可能依赖于算法DFS中第6行对结点进行检查的次序和算法DFS-VISIT的第5行对一个结点的邻接结点进行访问的次序。不过,这些不同的访问次序在实际中并不会导致问题,因为通常可以对任意的深度优先搜索结果加以有效利用,并获得等价的结果。

DFS的运行时间是多少?

如果排除调用DFS-VISIT的时间,第2-4行的循环和第6-8行的循环所需的时间为Θ(V)。就像对待广度优先搜索算法一样,在这里也使用聚合分析。对每个结点v∈V来说,DFS-VISIT被调用的次数刚好为一次,这是因为在对一个结点u调用DFS-VISIT时,该结点u必须是白色,而DFS-VISIT所做的第一件事情就是将结点u涂上灰色。在执行DFS-VISIT(G,v)的过程中,算法第5-8行的循环所执行的次数为|Adj[v]|。由于:
在这里插入图片描述
故执行DFS-VISIT第5-8行操作的总成本是Θ(E)。因此,深度优先搜索算法的运行时间为Θ(V+E)。

3.1 深度优先搜索的性质

深度优先搜索最基本的性质是,其生成的前驱子图Gπ形成一个由多棵树所构成的森林,这是因为深度优先树的结构与DFS-VISIT的递归调用结构完全对应。也就是说,u=v.π当且仅当DFS-VISIT(G,v)在算法对结点u的邻接链表进行搜索时被调用。此外,结点v是深度优先森林里结点u的后代当且仅当结点v在结点u为灰色的时间段里被发现。

深度优先搜索的另一个重要性质是,结点的发现时间和完成时间具有所谓的括号化结构(parenthesis structure)。如果以左括号“(u”来表示结点u的发现,以右括号“u)”来表示结点u的完成,则发现和完成的历史记载形成一个规整的表达式,这里“规整”的意思是所有的括号都适当地嵌套在一起。例如,对下图(记为图5)的a进行深度优先搜索所对应的括号化结构如图5的b所示:
在这里插入图片描述
在上图中:

1、a:有向图上的深度优先搜索结果。与【3 深度优先搜索】一节的图4一样,每个结点都有自己的时间戳,边的类型也在图中予以注明。

2、b:每个结点的发现时间和完成时间所构成的区间对应图中所示的括号化结构。每个矩形区域横跨由相应结点的发现和完成时间所给出的时间区间。图中给出的边都是树边(非树边被略去了)。如果两个时间区间存在重叠,则其中一个区间必定完全囊括在另一个区间内部,而对应较小区间的结点是对应较大区间的结点的后代。

3、c:对图5的a所进行的重画,该图给出了深度优先树中所有的树边、往下的从祖先指向后代的前向边和往上的从后代指向祖先的后向边。

下面的定理提供了另一种对括号化结构进行描述的方法。

定理7:(括号化定理)在对有向或无向图G=(V,E)进行的任意深度优先搜索中,对于任意两个结点u和v来说,下面三种情况只有一种成立:

1、区间[u.d,u.f]和区间[v.d,v.f]完全分离,在深度优先森林中,结点u不是结点v的后代,结点v也不是结点u的后代。

2、区间[u.d,u.f]完全包含在区间[v.d,v.f]内,在深度优先树中,结点u是结点v的后代。

3、区间[v.d,v.f]完全包含在区间[u.d,u.f]内,在深度优先树中,结点v是结点u的后代。

证明

1、u.d<v.d时。在该情况下,根据不等式v.d<u.f是否成立又可以分为两种子情况。第一种子情况是在v.d<u.f成立时,结点v在结点u仍然是灰色的时候被发现,这意味着结点v是结点u的后代。而且,因为结点v在结点u的后面被发现,其所有的出发边都已经被探索完,在搜索算法返回来继续处理结点u时,结点v的处理已经完成。在这种情况下,区间[v.d,v.f]完全包含在区间[u.d,u.f]内。在第二种子情况下,u.f<v.d。根据不等式u.d<u.f,有u.d<u.f<v.d<v.f,因此,区间[u.d,u.f]和区间[v.d,v.f]是完全分离的,没有一个结点是在另一个结点为灰色的时候被发现的,因此,没有一个结点是另一个结点的后代。

2、v.d<u.d时,证明过程类似,只不过将上述证明中的u和v进行对调即可。

定理7得证。

推论8:(后代区间的嵌套)在有向或无向图G的深度优先森林中,结点v是结点u的真后代当且仅当u.d<v.d<v.f<u.f成立。

证明:从定理7的第三种情况立即可得。

推论8得证。

下面的定理给出的是在深度优先森林中,当一个结点是另一个结点的后代时的另一个重要特征。

定理9:(白色路径定理)在有向或无向图G=(V,E)的深度优先森林中,结点v是结点u的后代当且仅当在发现结点u的时间u.d,存在一条从结点u到结点v的全部由白色结点所构成的路径。

证明

1、⇨:若v=u,则从结点u到结点v的路径仅包含结点u,而该结点在算法设置u.d的值时仍然是白色的。假定在深度优先森林中,结点v是结点u的真后代。根据推论8,有u.d<v.d,因此结点v在时刻u.d时为白色。由于结点v可以是u的任意后代,因此在深度优先森林中,从结点u到结点v的唯一简单路径上的所有结点在时刻u.d时都是白色的。

2、⇦:假定在时刻u.d时存在一条从结点u到结点v的全部由白色结点组成的路径,但结点v在深度优先树中却不是结点u的后代。不失一般性,假定从结点u到结点v的路径上除结点v以外的每个结点都成为u的后代(否则,可设v为路径上离结点u最近的没有成为结点u的后代的结点,两个假设是等价的)。设结点w为路径上结点v的前驱,使得w是u的一个后代或者w和u是同一个结点。根据推论8,有w.f≤u.f。因为结点v必须在结点u被发现之后但在结点w的处理完成之前被发现,所以u.d<v.d<w.f≤u.f。根据定理7,区间[v.d,v.f]完全包含在区间[u.d,u.f]中。根据推论8,结点v最后必然成为结点u的后代。

定理9得证。

3.2 边的分类

深度优先搜索的另一性质是,可以通过搜索来对输入图G=(V,E)的边进行分类,每条边的类型可以提供关于图的重要信息。对于在图G上运行深度优先搜索算法所生成的深度优先森林Gπ,可以定义4种边的类型:

1、树边:为深度优先森林Gπ中的边。如果结点v是因算法对边(u,v)的探索而首先被发现,则(u,v)是一条树边。

2、后向边:后向边(u,v)是将结点u连接到其在深度优先树中(一个)祖先结点v的边。由于有向图中可以有自循环,自循环也被认为是后向边。

3、前向边:是将结点u连接到其在深度优先树中一个后代结点v的边(u,v)。

4、横向边:指其他所有的边。这些边可以连接同一棵深度优先树中的结点,只要其中一个结点不是另外一个结点的祖先,也可以连接不同深度优先树中的两个结点。

在【3 深度优先搜索】一节的图4和【3.1 深度优先搜索的性质】一节的图5中,每条边上的标签标明了该条边的类型。图5的c同时还描述了如何对图5的a进行重画,以便让所有的树边和前向边都朝下指,而所有的后向边都朝上指。事实上,可以将任何图都重画成这种模式。

在遇到某些边时,DFS有足够的信息来对这些边进行分类。当第一次探索边(u,v)时,结点v的颜色能够告诉关于该条边的一些信息:

1、结点v为白色表明该条边(u,v)是一条树边。

2、结点v为灰色表明该条边(u,v)是一条后向边。

3、结点v为黑色表明该条边(u,v)是一条前向边或横向边。

第一种情况可以从算法的规范中立即推知。对于第二种情况,只要注意到,灰色结点总是形成一条线性的后代链,这条链对应当前活跃的DFS-VISIT调用栈;灰色结点的数量总是比深度优先森林中最近被发现的结点的深度多1。而算法对图的探索总是从深度最深的灰色结点往前推进,因此,(从当前灰色结点)通向另一个灰色结点的边所到达的是当前灰色结点的祖先。第三种情况处理的是剩下的可能性——边(u,v):① 在u.d<v.d时为前向边;② 在u.d>v.d时为横向边。

在对边进行分类时,无向图可能给算法带来一些模糊性,因为边(u,v)和边(v,u)实际上是同一条边。在这种情况下,将边(u,v)划分为分类列表中第一种适合该边的类型。也可以根据搜索时算法是先探索到边(u,v)还是边(v,u)来进行分类。

下面证明在对无向图的深度优先搜索中,从来不会出现前向边和横向边。

定理10:在对无向图G进行深度优先搜索时,每条边要么是树边,要么是后向边。

证明:设(u,v)是G的任意一条边。不失一般性,假定u.d<v.d。因为结点v在结点u的邻接链表中,搜索算法将在完成结点u的处理之前(即在结点u是灰色的时间段里)必定发现和完成对结点v的处理。如果在搜索算法第一次探索边(u,v)时,其方向是从结点u到结点v,则结点v在该时刻之前没有被发现(颜色为白色),否则,搜索算法将已经从反方向探索了这条边。因此,在这种情况下,(u,v)成为一条树边。如果搜索算法第一次探索边(u,v)时是从结点v到结点u的方向,则(u,v)是一条后向边,因为在边(u,v)被第一次探索时,结点u仍然是灰色的。

定理10得证。


4 拓扑排序

本节阐述如何使用深度优先搜索来对有向无环图进行拓扑排序。对于一个有向无环图G=(V,E)来说,其拓扑排序是G中所有结点的一种线性次序,该次序满足如下条件:如果图G包含边(u,v),则结点u在拓扑排序中处于结点v的前面。如果图G包含环路,则不可能排出一个线性次序。可以将图的拓扑排序看做是将图的所有结点在一条水平线上排开,图的所有有向边都从左指向右。许多实际应用都需要使用有向无环图来指明事件的优先次序。下图(记为图7)描述的是某人每天早上起床穿衣所发生的事件的次序图:
在这里插入图片描述
上图中:

1、a:某人对自己每天早上的穿衣进行的拓扑排序。每条有向边(u,v)表明服装u必须在服装v之前穿上。深度优先搜索的发现时间和完成时间注明在每个结点旁边。

2、b:以拓扑排序展示的同一个图,所有的结点按照其完成时间的逆序被排成从左至右的一条水平线。所有的有向边都从左指向右。

这个人必须先穿某些衣服,才能再穿其他衣服(如先穿袜子后才能再穿鞋)。有些服饰则可以以任意顺序穿上(如袜子和裤子之间可以以任意次序进行穿戴)。在图7的a所示的有向无环图中,有向边(u,v)表明服装u必须在服装v之前穿上。对该有向无环图进行拓扑排序所获得的就是一种合理穿衣的次序。图7的b将拓扑排序后的有向无环图在一条水平线上展示出来,在该水平线上,所有的有向边都从左指向右。

下面的简单伪代码可以对一个有向无环图进行拓扑排序:

TOPOLOGICAL-SORT(G)
    call DFS(G) to compute finishing times v.f for each vertex v
    as each vertex is finished, insert it onto the front of a linked list
    return the linked list of vertices

图7的b描述的是经过拓扑排序后的结点次序,这个次序与结点的完成时间的逆序相同。

因为深度优先搜索算法的运行时间为Θ(V+E),且将结点插入到链表最前端所需的时间为O(1),而一共只有|V|个结点需要插入,因此可以在Θ(V+E)的时间内完成拓扑排序。

下面的伪代码未使用DFS算法而对图G进行了拓扑排序:

TOPOLOGICAL-SORT(G)
    create a new empty list L //L用于保存拓扑排序的结果
    create a new empty set S //S用于保存图G中当前无入边的结点
    while S is not empty
        remove a node n from S
        insert n into L
        for each node m with an edge e from n to m
            remove edge e from G
            if m has no other incoming edges
                insert m into S
    if G has edges //若G还有边则有环,无法得到拓扑排序结果
        print('error')
    else
        print(L) //输出拓扑排序的结果L

下面的引理描述的是有向无环图的特征。

引理11:一个有向图G=(V,E)是无环的⇔对其进行的深度优先搜索不产生后向边。

证明

1、⇨:现在的条件是,有向图G=(V,E)是无环的。假定对图G进行的深度优先搜索产生了一条后向边(u,v),则在深度优先森林中,结点v是结点u的祖先。因此,图G包含一条从v到u的路径,该路径与后向边(u,v)一起形成一个环路。与条件矛盾,因此对G进行的深度优先搜索不能产生后向边;

2、⇦:现在的条件是,对G进行的深度优先搜索不产生后向边。假定G包含一个环路c。设结点v是环路c上第一个被发现的结点,设(u,v)是环路c中结点v前面的一条边。在时刻v.d,环路c中的结点形成一条从结点v到结点u的全白色结点路径(定理9)。根据白色路径定理,结点u将在深度优先森林中成为结点v的后代。因此,(u,v)是一条后向边。与条件矛盾,因此有向图G=(V,E)是无环的。

引理11得证。

下面的定理证明了拓扑排序算法的正确性。

定理12:拓扑排序算法TOPOLOGICAL-SORT生成的是有向无环图的拓扑排序。

证明:假定在有向无环图G=(V,E)上运行DFS来计算结点的完成时间。只需要证明,对于任意一对不同的结点u,v∈V,如果图G包含一条从结点u到结点v的边,则v.f<u.f。考虑算法DFS(G)所探索的任意一条边(u,v)。当这条边被探索时,结点v不可能是灰色,因为那样的话,结点v将是结点u的祖先,这样(u,v)将是一条后向边,与引理11矛盾。因此,结点v要么是白色,要么是黑色。如果结点v是白色,它将成为结点u的后代,因此v.f<u.f。如果结点v是黑色,则对其全部的处理都已经完成,因此v.f已经被设置。因为还需要对结点u进行探索,u.f尚需要设定。一旦对u.f进行设定,则其数值必定比v.f大,即u.f>v.f。因此,对于任意一条边(u,v),有v.f<u.f。

定理12得证。


5 强连通分量

不会更新


END

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值