算法导论 总结索引 | 第五部分 第二十二章:基本的图算法

图的搜索 指的是 系统化地跟随图中的边 来访问图中的每个结点。图搜索算法 可以用来发现图的结构。许多的图算法 在一开始都会 先通过搜索来获得图的结构,其他的一些图算法 则是对基本的搜索加以优化。图的搜索技巧 是整个图算法领域的核心

1、图的表示

1、对于图 G=(V, E),可以用 两种标准表示方法 表示。一种表示法 将图作为邻接链表的组合,另一种表示法 则将图作为邻接矩阵 来看待。两种表示方法 都既可以表示 无向图,也可以表示 有向图

邻接链表 因为在表示稀疏图(边的数量E 远远小于 |V|2 的图)时非常紧凑 而成为通常的选择
在稠密图(|E| 接近 |V|2 的图)的情况下,可能倾向于 使用邻接矩阵表示法。如果需要 快速判断 任意两个节点之间 是否有边相连,也可能需要 使用邻接矩阵表示法

2、对于图 G=(V, E),其 邻接链表表示 由一个包含 |V| 条链表的数组 Adj 来构成,每个结点 有一条链表。对于每个结点 u∈V,Adj[u] 包含 G 中所有与 u 邻接的结点 (也可说 该链表里包含 指向这些结点的指针) 。由于 邻接链表代表的是 图的边,在伪代码里,将 数组Adj 看做是图的一个属性
在这里插入图片描述
3、如果 G 是有向图,对于边 (u, v) 来说,结点 v 将出现在链表 Adj[u] 里,所有邻接链表的长度之和 等于 |E|。如果 G 是一个无向图,则对于边 (u, v) 来说,结点 v 将出现在 链表Adj[u] 里,结点 u 将出现在 链表 Adj[v] 里,因此,所有邻接链表的长度之和 等于 2|E|。但不管是 有向图还是无向图,邻接链表表示法的存储空间需求 都为Θ(V + E)
在这里插入图片描述
对邻接链表 稍加修改,即可以用来表示 权重图。权重图 是图中的每条边 都带有一个相关的权重 的图。设 G=(V, E) 为一个权重图,其权重函数为 w,可以直接将边 (u, v)∈E 的权重值 w(u, v) 存放在结点 u 的邻接链表里。邻接链表表示法的鲁棒性很高,可以对其进行简单修改 来支持许多其他的图变种

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

5、图G的邻接矩阵表示由一个 |V|×|V| 的矩阵 A=(a_ij) 表示
在这里插入图片描述
无论 一个图有多少条边,邻接矩阵的空间需求 皆为 Θ(V2)

无向图的邻接矩阵是 一个对称矩阵。在某些应用中,可能只需要 存放对角线 及其以上的这部分邻接矩阵(即半个矩阵),从而将图存储空间需求 减少几乎一半

与 邻接链表表示法 一样,邻接矩阵 也可以用来表示 权重图。例如,如果 G=(V, E) 为一个权重图,并且其权重函数为 w,则我们直接 将边 (u,v)∈E 的权重 w(u, v) 存放在 邻接矩阵A的第 u 行第 v 列记录上。对于不存在的边,则在相应的位置上放置 NIL。不过,对于许多问题来说,使用 0 或者 ∞ 来表示 一条不存在的边可能更为便捷

邻接链表表示法 和 邻接矩阵表示法 在渐近意义下 至少是一样空间有效的,但 邻接矩阵表示法 更为简单。因此,在图规模比较小的时候,可能 更倾向于 邻接矩阵表示法。而且,对于 无向图 来说,邻接矩阵 还有一个优势:每个记录项 只需要 1 位的空间

1.1 表示图的属性

1、如果使用邻接链表 来表示图,一种可能的方法是 使用附加的数组来 表示结点和边的属性。如 一个与 Adj 数组相对应的数组 d[1…|V|]。如果与 u 邻接的结点 都在 Adj[u] 中,则 属性 u.d 将存放在 数组项 d[u] 里。还有
许多其他方法 可以用来实现 属性的表示。例如,在面向对象的程序设计语言里,结点属性 可以表示为 Vertex 类下面的一个子类中的实例变量

22.1-5 向图 G=(V, E) 的平方图 是图 G2 = (V, E2),这里,边 (u,v)∈E2 当且仅当图 G 包含合一条 最多由两条边构成的 从 u 到 v 的路径。请描述一个有效算法来计算图 G 的子图G2。这里图 G 既可以以邻接链表表示,也可以以邻接矩阵表示。请分析算法运行时间

在这里插入图片描述

for each v ∈ Adj[u]
    INSERT(Adj2[u], v)
    for each w ∈ Adj[v]
        // 边 (u, w) ∈ E^2
        INSERT(Adj2[u], w)

在这里插入图片描述
在这里插入图片描述
22.1-7 有向无环图 G=(V, E) 的关联矩阵是 一个满足下述条件的 |V|×|E| 矩阵 B=(b_ij)
在这里插入图片描述
在这里插入图片描述
22.1-8 假定数组 Adj[u] 每个记录项 不是链表,而是 一个散列表,里面装的是 (u, v)∈E 的结点 v。如果每条边被查询的概率相同,则判断一条边 是否在图中的 期望时间值是多少?这种表示方式的缺陷是什么?请为每条链表 给出一个不同的数组结构 来解决这个问题。与散列表 相比较,所给出的新方法存在什么缺陷吗?
在这里插入图片描述
散列表的关键概念:
在这里插入图片描述
在这里插入图片描述

2、广度优先搜索

1、Prim的最小生成树算法 和 Dijkstra的单源最短路径算法 都使用了类似广度优先搜索的思想

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

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

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

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

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

BFS(G,s)
1 for each vertex u ∈ G.V - {s}
2 	u.color = WHITE
3	u.d =4	u.π = NIL
5 s.color = GRAY
6 s.d = 0
7 s.π = NIL
8 Q =9 ENQUEUE(Q,s)
10 while Q ≠ ∅
11 	u = DEQUEUE(Q)
12 	for each v ∈ G.Adj[u]
13 	if v.color == WHITE
14    v.color = GRAY
15    v.d = u.d + 1
16    v.π = u
17    ENQUEUE(Q, v)
18 	u.color = BLACK

在这里插入图片描述
while循环 一直执行到 队列中不再有灰色结点时结束。如前所示,灰色结点 指的是已被发现的结点,但其邻接链表 尚未被完全检查

由于一个结点在涂上灰色(第 14 行)的同时 也被加入队列 Q 中(第 17 行),而结点 在从队列里删除(第 11 行)的同时被涂上黑色(第18行)
广度优先搜索的结果 可能依赖于 对每个结点的邻居结点的访问顺序(第 12 行): 广度优先树可能会不一样,但本算法所计算出来的距离 d 都是一样的

2.1 分析

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

2.2 最短路径

1、定义从源结点 s 到结点 v 的最短路径距离 δ(s,v) 为从结点 s 到结点 v 之间所有路径中最少的步数。如果从结点 s 到结点 v 之间没有路径,则 δ(s,v) = ∞ 。称从结点 s 到结点 v 的长度为 δ(s,v) 的路径 s 到 v 的最短路径

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

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

3、引理 22.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)

因为 s.d = 0 = δ(s, s),并且对于所有的结点 v∈V - {s},v.d = ∞ ≥ δ(s, v),所以归纳假设成立
考虑从结点 u 进行邻接链表搜索时 所发现的白色结点 v。根据归纳假设,有 u.d ≥ δ(s,u)。从算法 v.d = u.d + 1 的赋值操作 和 引理 22.1 可知,

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

结点 v 在这之后 被加入到队列 Q 里面,并且因为 v 在入队时 涂上灰色就不会再次入队,因此,第 14~17 行 仅在白色结点上执行。所以,v.d 的值不再会发生变化,我们的归纳假设成立

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

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

对于归纳步,必须证明 在入队和出队操作时,引理都成立
如果头结点 v1 被删除,v2 将变为队列里新的头结点(如果队伍 在删除头结点后为空,则 引理直接成立)。根据归纳假设,有 v1.d ≤ v2.d(上一轮的(2))。有 vr.d ≤ v1.d + 1(上一轮的(1))≤ v2.d + 1(之前的 v1.d ≤ v2.d 结论 ),且其下的不等式不受影响(队列中的其他元素没变)。因此,在 v2 为头结点时引理依然成立

在结点加入队列时,在算法的第 17 行将结点 v 加入队列时,该结点成为结点 v_r+1。在这个时候,已经删除了结点 u,并正在对该结点的邻接链表 进行检查。根据归纳假设,新的头结点 v1 满足 v1.d ≥ u.d(上一轮中结论(2))。因此,vr+1.d = v.d = u.d + 1 ≤ v1.d + 1。根据归纳假设,还有 vr.d ≤ u.d + 1(上一轮的结论(1)), 因此,v_r.d ≤ u.d + 1 = v.d + 1 = v_r+1.d。余下的不等式不受影响。因此,当结点 v 加 入队列时 引理仍然成立

5、在结点加入队列时,d 值随时间推移单调增长
推论 22.4 假定在执行 BFS 时,结点 vi 和结点 vj 都加入到队列 Q 里,并且 vi 在 vj 之前入队,则在 vj 入队时,我们有 vi.d ≤ vj.d

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

6、定理 22.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。根据引理 22.2,v.d ≥ δ(s, v),因此有 v.d > δ(s, v)
另外结点 v 必定是从 s 可以达到的,因为 如果不是这样,则会出现 δ(s, v) = ∞ >= v.d(违背引理 22.2)
设 u 为从源结点 s 到结点 v 的最短路径上 结点 v 的直接前驱结点,则 δ(s, v) = δ(s, u) + 1。因为 δ(s, u) < δ(s, v),并且因为 对结点 v 的选择,所以有 u.d = δ(s, u)。将这些分析合并起来:

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

不管 v 是何种颜色的结点,我们都将导出与 上述不等式 矛盾的情形

如果 v 是白色结点,则算法的第 15 行将设置 v.d = u.d+1,这与 上述不等式矛盾。如果 v 是黑色,则该结点已经队队列里删除,根据推论 22.4,有 v.d ≤ u.d,再次与不等式矛盾。如果 v 是灰色,则 v 是在某个结点 w 出队时被涂上灰色的,而结点 w 在结点 u 之前出队,并且 v.d = w.d+1。根据推论 22.4,w.d ≤ u.d,因此有 v.d = w.d+1 ≤ u.d+1,这再次与不等式相矛盾
对于所有的 v∈V,v.d = δ(s, v)

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

2.3 广度优先树

1、对于图 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 之间的一条最短路径,则称 Eπ 中的边为树边,因为它是连通的,并且 |Eπ| = |Vπ|-1

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

证明:如果结点 v 可以从源结点 s 到达,Vπ 由从源结点 s 可以到达的 V 集合里面的结点所组成。由于 Gπ 形成一棵树,可以获得 每条这样的路径 也是图 G 里面的一条最短路径

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

PRINT-PATH(G, s, v)
1 if s == v
2 	print "s"
3 else if v.π == NIL
4 	print "no path from "s" to "v" exists"
5 else PRINT-PATH(G, s, v.π)
6 	print "v"

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

22.2-3 使用单个位 来存放每个结点的颜色即可。算法第 18 行的伪代码删除后,BFS 过程生成的结果不变,因为变成黑色时已经出队了,循环条件 仅仅是 结点是否是白色

22.2-6
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
BFS 不能生成所有可能的 最短路径树

22.2-7 有两种类型的职业摔跤手:“好人”(babyfaces)和“坏人”(heels)。在任意一对职业摔跤手之间,可能存在或不存在对立关系。假设我们有 n 个职业摔跤手,并且我们有一个包含 r 对摔跤手的列表,这些摔跤手之间存在对立关系。设计一个 O(n+r) 时间复杂度的算法,确定是否可以将其中一些摔跤手指定为好人,而其余的摔跤手指定为坏人,使得每个对立关系都发生在一个好人和一个坏人之间。如果可以进行这样的划分,算法应该输出该划分方案

这个问题基本上 是一个伪装的二分图着色问题。我们将尝试用两种颜色“好人”和“坏人” 来给这个对立关系图的顶点着色。保证 没有两个好人之间 或 没有两个坏人之间 存在对立关系 等同于说明着色是正确的。为了进行二分图着色,我们对每个连通分量进行广度优先搜索,以获得每个顶点的 d 值。然后,我们将所有 d 值为奇数的顶点涂为一种颜色,比如“坏人”,将所有 d 值为偶数的顶点涂为另一种颜色

因为如果我们给了顶点 v 和它的父节点 v.π 相同的颜色,那么 v 和 v.π 的 d 值必须具有不同的奇偶性。只需要检查每条边,看这种着色是否有效。如果每条边 都满足条件,就可以找到一个划分;如果有一条边不满足条件,则不可能找到一个划分。由于 BFS 需要 O(n+r) 的时间,而检查每条边也需要 O® 的时间,总运行时间为 O(n+r)

22.2-9 设 G = (V,E) 是一个迷宫,其正反方向通过 E 中每条边恰好一次(该路径通过每条边两次,这两条边的方向相反)。请描述如何在迷宫中找出一条路
在这里插入图片描述

MAKE-PATH(u)
    for each v ∈ Adj[u] but not in the tree such that u ≤ v
    对于每个在顶点 u 的邻接表 Adj[u] 中的顶点 v,如果 v 不在生成树中,并且 u ≤ v
        go to v and back to u
    for each v ∈ Adj[u] but not equal to u.π
        go to v
        perform the path proscribed by MAKE-PATH(v)
    go to u.π

3、深度优先搜索

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

2、像广度优先搜索一样,在对已被发现的结点 u 的邻接链表 进行扫描时,每当发现一个结点 v 时,深度优先搜索算法 将对这个事件 进行记录,将 v 的前驱属性 v.π 设置为 u

不过,与广度优先搜索 不同的是,广度优先搜索的前驱子图 形成一棵树,而深度优先搜索的前驱子图 可能由多棵树组成,因为搜索 可能从多个源结点 重复进行
设图 Gπ = (V, Eπ),其中 Eπ = {(v.π, v): v∈V 且 v.π ≠ NIL}。深度优先搜索的前驱子图 形成为 一个由多棵深度优先树 构成的深度优先森林。森林 Eπ 中的边 仍然称为树边

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

深度优先搜索算法 还在每个结点 盖上一个时间戳。每个结点 v 有两个时间戳:第一个时间戳 v.d 记录结点 v 第一次被发现的时间 (涂上灰色的时候),第二个时间戳 v.f 记录的是 搜索完成对 v 的邻接链表扫描的时候(涂上黑色的时候)

4、将其发现结点 u 的时刻 记录在属性 u.d 中,将其完成对结点 u 处理的时刻 记录在属性 u.f 中。因为 |V| 个结点中 每个结点 只能有一个发现事件和一个完成事件,所以 这些时间戳都是处于 1 和 2|V| 之间的整数。很显然,对于每个结点 u,我们有:
u.d < u.f
结点 u 在时刻 u.d 之前为白色,在时刻 u.d 和 u.f 之间为灰色,在时刻 u.f 之后为黑色

输入图 G 既可以是 无向图,也可以是 有向图。变量 time 是一个全局变量,用来计算时间戳

DFS(G)
1 for each vertex u ∈ G.V
2 	u.color = WHITE
3 	u.π = NIL
4 time = 0
5 for each vertex u ∈ G.V
6 	if u.color == WHITE
7 		DFS-VISIT(G, u)

DFS-VISIT(G, u)
1 time = time + 1 // white vertex u has just been discovered
2 u.d = time
3 u.color = GRAY
4 for each v ∈ G.Adj[u] // explore edge (u, v)
5 	if v.color == WHITE
6 		v.π = u
7 		DFS-VISIT(G, v)
8 u.color = BLACK // blacken u; it is finished
9 time = time + 1
10 u.f = time

深度优先搜索的结果 可能依赖于算法 DFS 中第 5 行 对节点进行检查的次序 和 算法 DFS-VISIT 的第 4 行对一个节点的邻接节点 进行访问的次序。因为 通常可以对任意的深度优先搜索结果 加以有效利用,并获得 等价的结果
在这里插入图片描述
DFS 的运行时间:
如果排除 调用 DFS-VISIT 的时间,第 1~3 行的循环 和 第 5~7 行的循环所需的时间为 Θ(V)。就像 对待广度优先搜索算法一样,在这里 也使用聚合分析。对每个结点 v ∈ V 来说,DFS-VISIT 被调用的次数刚好为一次,这是因为 在对一个结点 u 调用 DFS-VISIT 时,该结点 u 必须是白色,而 DFS-VISIT 所做的第一件事情 就是将结点 u 涂上灰色
在执行 DFS-VISIT(G, v) 的过程中,算法第 4~7 行的循环所执行的次数为 |Adj[v]|。由于 ∑v∈V |Adj[v]| = Θ(E),执行 DFS-VISIT 第 4~7 行操作的总成本是 Θ(E)。因此,深度优先搜索算法的运行时间为 Θ(V + E)

3.1 深度优先搜索的性质

1、u = v.π 当且仅当 DFS- VISIT (G, v) 在算法对结点 u 的邻接链表 进行搜索时被调用。此外,结点 v 是深度优先森林里 结点 u 的后代 当且仅当结点 v 在结点 u 为灰色的时间段里被发现

2、结点的发现时间和完成时间 具有所谓的括号化结构。如果以左括号 “(u” 来表示结点 u 的发现,以右括号 “u)” 来表示结点 u 的完成,则发现和完成的历史记载 形成一个规整的表达式,这里“规整”的意思是 所有的括号都适当地嵌套在一起
在这里插入图片描述
深度优先搜索在图中的 括号结构为:
(u(v(y(xx)y)v)u)(w(zz)w)

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

  • 区间 [u.d, u.f] 和区间 [v.d, v.f] 完全分离,在深度优先森林中,结点 u 不是结点 v 的后代,结点 v 也不是结点 u 的后代
  • 区间 [u.d, u.f] 完全包含在区间 [v.d, v.f] 内,在深度优先树中,结点 u 是结点 v 的后代
  • 区间 [v.d, v.f] 完全包含在区间 [u.d, u.f] 内,在深度优先树中,结点 v 是结点 u 的后代

证明:从 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(同理 v.d < v.f),我们有 u.d < u.f < v.d < v.f,因此,区间 [u.d, u.f] 和区间 [v.d, v.f] 是完全分离的,没有一个结点 是在另一个结点为灰色的时候 被发现的,因此,没有一个结点 是另一个结点的后代

4、推论 22.8 (后代区间的嵌套) 在有向或无向图 G 的深度优先森林中,结点 v 是结点 u 的真后代 当且仅当 u.d < v.d < v.f < u.f 成立(由上一条易得)

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

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

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

3.2 边的分类

1、对于在图 G 上 运行深度优先搜索算法 所生成的深度优先森林 G_π,可以定义 4 种边的类型:

  1. 树边:为深度优先森林 G_π 中的边。如果结点 v 是因算法对边 (u, v) 的探索 而首先被发现,则 (u, v) 是一条树边
  2. 后向边:后向边 (u, v) 是将结点 u 连接到其在深度优先树中(一个)祖先结点 v 的边。由于有向图中 可以有自循环,自循环 也被认为是后向边
  3. 前向边:是将结点 u 连接到其在深度优先树中 一个后代结点 v 的边 (u, v)
  4. 横向边:指其他所有的边。这些边 可以连接同一棵深度优先树中的结点,只要 其中一个结点不是另外一结点的祖先,也可以 连接不同深度优先树中 的两个结点

2、当 第一次探索边 (u, v) 时(即 u 为深度最深的灰色结点),结点 v 的颜色 能够告诉我们关于该条边的一些信息

  1. 结点 v 为白色表明该条边 (u, v) 是一条树边
  2. 结点 v 为灰色表明该条边 (u, v) 是一条后向边
  3. 结点 v 为黑色表明该条边 (u, v) 是一条 前向边(v.f > u.f,原因见 22.3-5) 或 横向边(其余情况)

对于第二种情况,只要注意到,灰色结点 总是形成一条线性的后代链,这条链 对应当前活跃的 DFS-VISIT 调用栈;灰色结点的数量 总是 比深度优先森林 最近被发现的结点(白变灰)的深度多 1。而算法对图的探索 总是从深度最深的灰色结点 往前推进,因此,(从当前灰色结点)通向另一个灰色结点的边 所到达的是当前灰色结点的祖先

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

4、定理 22.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 仍然是灰色的

22.3-1 画一个 3×3 的网格,行和列的抬头 分别标记为白色、灰色和黑色。对于 每个表单元 (i, j),指出 在对有向图进行 深度优先搜索的过程中,是否存在一条边,连接一个颜色为 i 的结点 和 一个颜色为 j 的结点。对于每种可能的边,指出该种边的类型(不是第一次探索边了)
另外,请针对无向图的深度优先搜索 再制作一张这样的网格
在这里插入图片描述
在这里插入图片描述
22.3-2 给出深度优先搜索算法 在图上的运行过程。假定深度优先搜索算法的第 5~7 行的 for 循环是 以字母表顺序依次处理 每个结点,并假定 每条邻接链表以字母表顺序对里面的结点进行了排序。请给出 每个结点的发现时间 d[i] 和完成时间 f[i] ,并给每条边分类
在这里插入图片描述
在这里插入图片描述
22.3-4 证明:使用单个位来 存放每个结点的颜色已经足够。这一点 可以通过证明如下事实来得到:如果将 DFS-Visit 的第 2 行删除,DFS 给出的结果相同

同 2.3 22.2-3

22.3-5 边 (u, v) 是:
a. 树边 或 前向边当且仅当 u.d < v.d < v.f < u.f (u 是 v 的祖先)
b. 后向边 当且仅当 v.d ≤ u.d < u.f ≤ v.f (u 是 v 的后代)
c. 横向边 当且仅当 v.d < v.f < u.d < u.f (v 在 u 之前 已经遍历完成,因为 既然是边(u, v)不可能 u 在 v 之前,不然就构成祖先关系了)

22.3-6 证明:在无向图中,根据 深度优先搜索算法 是先探索 (u, v) 还是 先探索 (v, u) 来将边 (u, v) 分类为树边 或者 后向边,与根据分类列表中的 4 种类型的次序 进行分类是等价的(树,后向,前向,横)

根据定理 22.10,无向图的每一条边要么是树边,要么是后向边。从先探索 (v, u) 来将边 (u, v) 分类为树边 或者 后向边 开始
首先假设顶点 v 是通过探索边 (u, v) 首先被发现的。那么根据定义,(u, v) 是一条树边。此外,(u, v) 必须在 (v, u) 之前被发现,因为一旦 (v, u) 被探索,v 必然被发现
现在假设 v 不是通过 (u, v) 首先被发现的,那么它一定是通过某个 r ≠ u 的 (r, v) 的边被发现的。如果 u 还没有被发现,那么如果 (u, v) 是首先被探索的边,它一定是一条后向边,因为 v 是 u 的祖先。如果 u 已经被发现,那么 u 是 v 的祖先,因此 (v, u) 是一条后向边

2.3-7 请重写 DFS 算法的伪代码,以便 使用栈来消除递归调用

DFS-STACK(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-STACK(G, u)
DFS-VISIT-STACK(G, u)
    S = Ø
    PUSH(S, u)
    time = time + 1             // white vertex u has just been discovered
    u.d = time
    u.color = GRAY
    while !STACK-EMPTY(S)
        u = TOP(S)
        v = FIRST-WHITE-NEIGHBOR(G, u)
        if v == NIL
            // u's adjacency list has been fully explored
            POP(S)
            time = time + 1
            u.f = time
            u.color = BLACK     // blackend u; it is finished
        else
            // u's adjacency list hasn't been fully explored
            v.π = u
            time = time + 1
            v.d = time
            v.color = GRAY
            PUSH(S, v)
FIRST-WHITE-NEIGHBOR(G, u)
    for each vertex v ∈ G.Adj[u]
        if v.color == WHITE
            return v
    return NIL

22.3-10 修改 深度优先搜索的伪代码,让它打印出 有向图 G 的每条边及其分类。并指出,如果 图 G 是无向图,要进行何种修改 才能达到相同的效果

依据为 3.2、2

DFS-VISIT-PRINT(G, u)
    time = time + 1
    u.d = time
    u.color = GRAY
    for each vertex v ∈ G.Adj[u]
        if v.color == WHITE // 3.2 2、a
            print "(u, v) is a tree edge."
            v.π = u
            DFS-VISIT-PRINT(G, v)
        else if v.color == GRAY // 3.2 2、b
            print "(u, v) is a back edge."
        else if v.f > u.f // 3.2 2、c
            print "(u, v) is a forward edge."
        else
            print "(u, v) is a cross edge."
    u.color = BLACK
    time = time + 1
    u.f = time

22.3-11 请解释有向图的一个结点 u 怎样才能成为 深度优先树中的唯一结点,即使结点 u 同时有入边和出边

考虑一个具有三个顶点 u、v 和 w 的图,并且具有边 (w, u)、(u, w) 和 (w, v)。假设深度优先搜索(DFS)首先探索 w,并且 w 的邻接列表中 u 在 v 之前。接下来我们发现 u。唯一的相邻顶点是 w,但 w 已经是灰色,所以 u 完成。由于 v 还不是 u 的后代 并且 u 已经完成,v 永远不可能成为 u 的后代

22.3-12 证明:可以在在无向图 G 上使用深度优先搜索 来获得图 G 的连通分量,并且 深度优先森林所包含的树的棵数 与 G 的连通分量数量相同
给出 如何修改深度优先搜索 来让其给每个结点赋予 一个介于 1 和 k 之间的整数值 v.cc,这里 k 是 G 的总连通分量的个数,使得 u.cc = v.cc 当且仅当结点 u 和结点 v 处于同一个连通分量中

当图 G 是连通的,即所有节点 都可以从任意节点到达,那么 DFS 将会生成一个单一的深度优先树。而当 G 是非连通的,即存在多个连通分量时,DFS 将会为 每个连通分量生成一棵树,这些树的集合 被称为深度优先森林
证明过程:
连通性:对于每个连通分量,DFS 从该连通分量的任意一个节点出发,遍历 该连通分量的所有节点,并将它们标记为同一连通分量(即 u.cc = cc)
连通分量数量与树的数量一致:DFS 只能在不同的连通分量中 分别启动新的搜索,因为 如果某个连通分量中的某个节点已经被访问过,DFS 就不会重新启动。因此,DFS 生成的树的数量 正好等于图的连通分量数量 k

如何修改深度优先搜索:
每次在 DFS-CC 的第 7 行的条件满足时,在森林中有一个新的树的根,因此我们更新其 cc 标签为 k 的新值。在对 DFS-VISIT-CC 的递归调用中,总是更新后代的连通分量使其与祖先的一致

DFS-CC(G)
1    for each vertex u ∈ G.V
2        u.color = WHITE
3        u.π = NIL
4    time = 0
5    cc = 1
6    for each vertex u ∈ G.V
7        if u.color == WHITE
8            u.cc = cc
9            cc = cc + 1
10           DFS-VISIT-CC(G, u)
DFS-VISIT-CC(G, u)
    time = time + 1
    u.d = time
    u.color = GRAY
    for each vertex v ∈ G.Adj[u]
        if v.color == WHITE
            v.cc = u.cc
            v.π = u
            DFS-VISIT-CC(G, v)
    u.color = BLACK
    time = time + 1
    u.f = time

4、拓扑排序

1、如何使用深度优先搜索 来 对有向无环图进行 拓扑排序。对于一个有向无环图 G = (V, E) 来说,其拓扑排序是 G 中所有结点的一种线性次序,该次序满足如下条件:如果图 G 包含边 (u, v),则结点 u 在拓扑排序中 处于结点 v 的前面(如果图 G 包含环路,则不可能 排出一个线性次序)。可以将图的拓扑排序 看做是将图的所有结点在一条水平线上排开,图的所有有向边 都从左指向右
因此,拓扑排序 与本书第二部分所讨论的通常意义上的 “排序” 是不同的

2、使用 有向无环图 来指明事件的优先次序。图22-7 描述的是 Bumstead 教授每天早上起床穿衣 所发生的事件的次序图。教授 必须先穿某些衣服,才能 再穿其他衣服 (如先穿袜子 后才能再穿鞋)。有些服饰 则可以任意顺序穿上 (如袜子和裤子之间 可以以任意次序进行穿戴)。在图 22-7(a) 所示的有向无环图中,有向边 (u, v) 表明服装 u 必须在服装 v 之前穿上

对该 有向无环图 进行拓扑排序 所获得的就是一种合理穿衣的次序。图 22-7(b) 将拓扑排序后的有向无环图 在一条水平线上展示出来,在该水平线上,所有的有向边 都从左指向右
在这里插入图片描述
对一个有向无环图 进行拓扑排序:

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

图 22-7(b)描述的是 经过拓扑排序后的结点次序,这个次序 与结点的完成时间恰好相反

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

3、将使用下面的引理 来证明 拓扑排序算法的正确性,该引理描述的是 有问无环图的特征

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

证明
=>:假定对图 G 进行的深度优先搜索产生了 一条后向边 (u, v),则在深度优先森林中,结点 v 是结点 u 的祖先。因此,图 G 包含一条从 v 到 u 的路径,该路径与后向边 (u, v) 一起形成一个环路

<=:假设 G 包含一个环路 c。下面来证明 深度优先搜索 将产生一条后向边。设结点 v 是环路 c 上(包括结点 u, v)第一个被发现的结点,设 (u, v) 是环路 c 中结点 v 前面的一条边。在时刻 v.d,环路 c 中的结点 形成一条从结点 v 到结点 u 的全白色结点路径
根据白色路径定理,结点 u 将在深度优先森林中 成为结点v的后代。因此,(u, v) 是一条后向边

4、定理22.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) 将是一条后向边,与 引理2.11(就不是无环图了) 矛盾。因此,结点 v 要么是白色,要么是黑色
如果结点 v 是白色,它将成为结点 u 的后代,因此 v.f < u.f。如果结点 v 是黑色,则对其全部的处理都已经完成,因此 v.f 已经被设置。因为需要对结点 u 进行探索,u.f 尚需设定。但一旦对 u.f 进行设定,则其数值必定比 v.f 大,即 v.f < u.f。因此,对于任意一条边 (u, v),有 v.f < u.f

22.4-1 循环是 以字母表顺序依次处理 每个结点,给出算法 TOPOLOGICAL-SORT 运行于图 22-8 上时所生成的结点次序

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

在这里插入图片描述
在这里插入图片描述
完全按照 label.f 逆序排序

22.4-2 请给出 一个线性时间的算法,算法的输入为 一个有向无环图 G = (V, E) 以及 两个结点 s 和 t, 算法的输出是 从结点 s 到结点 t 之间的简单路径的数量。例如,对于图 22-8 所示的有向无环图,从结点 p 到结点 v — 共有 4 条简单路径,分别是 pov、poryu、posryu 和 psryu。(本题仅 要求计数简单路径的条数,而不要求将简单路径本身列举出来)

算法的工作原理如下:节点 u 的属性 u.paths 表示从 u 到 v 的简单路径的数量,其中 假设 v 在整个过程中 是固定的。首先,应进行拓扑排序,并将位于 u 和 v 之间的顶点列为 {v[1], v[2], …, v[k-1]}。为了计算路径数量,应从 v 到 u 构建解决方案。将 u 称为 v[0],将 v 称为 v[k],为避免重复子问题,v[k] 和 u 之间的路径数量应被记住(DP数组),并随着 k 的减少 使用这些数量。在 Θ(V + E) 的时间复杂度内解决问题

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

SIMPLE-PATHS(G, u, v)
    TOPOLOGICAL-SORT(G)
    let {v[1], v[2]..v[k - 1]} be the vertex between u and v
    v[0] = u
    v[k] = v
    for j = 0 to k - 1
        DP[j] = ∞
    DP[k] = 1 // 开始只有1条路,DP是记录数量的
    return SIMPLE-PATHS-AID(G, DP, 0)

SIMPLE-PATHS-AID(G, DP, i)
    if i > k
        return 0
    else if DP[i] !=return DP[i]
    else
       DP[i] = 0
       for v[m] in G.adj[v[i]] and 0 < m ≤ k // 在目标结点范围内,又是 v 数组的邻接结点
            DP[i] += SIMPLE-PATHS-AID(G, DP, m)
       return DP[i]

22.4-4 证明或证伪 下述论断:如果有向图 G 包含环路,则在算法 TOPOLOGICAL-SORT(G) 所生成的结点序列顺序 与 图中边的指向不一致(即“坏” 边)的数量最少
在这里插入图片描述

5、强连通分量

1、考虑 深度优先搜索的一个经典应用:将有向图 分解为 强连通分量。使用深度优先搜索来做到这一点。许多针对有向图的算法都以 此种分解操作开始。在将图分解为 强连通分量后,这些算法 将分别运行在每个连通分量上,然后 根据连通分量之间的连接结构 将各个结果组合起来

G, GT, GSSC
在这里插入图片描述

STRONGLY-CONNECTED-COMPONENTS(G)
1 调用 DFS(G) 来计算每个顶点 u 的完成时间 u.f
2 计算 G^T(即图 G 的转置图)
3 调用 DFS(G^T),但在 DFS 的主循环中,根据第1步中计算的 u.f 递减顺序来考虑顶点
4 将第3步中形成的深度优先森林中的每棵树的顶点 作为一个单独的强连通分量输出

上述算法背后的思路来自于分配图 GSCC = (VSCC, ESCC) 的一个关键性质:假定图 G 有强连通分量 C1, C2, …, Ck。顶点集 VSCC 为 {v1, v2, …, vk},对于图 G 的每个强连通分量 Ci 来说,该集合 包含代表该分量的结点 vi。如果对于某个 x∈Ci 和 y∈Cj,图 G 包含一条有向边 (x,y),则边 (vi, vj) ∈ ESCC
从另一个角度来看,通过收缩所有相邻结点 都在同一个强连通分量中的边,剩下的图就是 GSCC。图 22-9© 描述的就是图 22-9(a) 的分量图

分量图的关键性质是:分量图是一个有向无环图
引理 22.13 设 C 和 C′ 为有向图 G = (V, E) 的两个不同的强连通分量,设结点 u, v ∈ C,结点 u′, v′ ∈ C′,假设图 G 包含一条从顶点 u 到顶点 u′ 的路径 u⇝u′。那么图 G 不可能包含一条 从顶点 v′ 到顶点 v 的路径 v′⇝v

证明:如果图 G 包含一条从顶点 v′ 到顶点 v 的路径 v′⇝v,则 G 将包含路径 u⇝u′⇝v′(u⇝u′(引理条件假设) + u’⇝v’(连通))和 v′⇝v⇝u(v’⇝v(证明条件假设) + v⇝u(连通))。因此,u 和 v′ 可以互相到达(将两个路径连起来),从而与 C 和 C′ 是不同的强连通分量的假设矛盾

3、在进行 第二次深度优先搜索时,以第一次深度优先搜索 所计算出的顶点完成时间的递减顺序 来对顶点进行考察,实际上是在 以拓扑排序的次序 来访问分量图中的顶点(每个顶点对应图 G 的一个强连通分量)
因为算法 STRONGLY-CONNECTED-COMPONENTS 执行两次深度优先搜索,u.d 或 u.f 指的是 第一次深度优先搜索(算法第 1 行)所计算出的发现时间和完成时间

将 结点的发现时间和完成时间的概念 推广到 结点集合上:如果结点集合 U ⊆ V,则定义 d(U) = min u∈U {u.d} 和 f(U) = max u∈U {u.f}。也就是说,d(U) 和 f(U) 分别是结点集合 U 中 所有结点里 最早的发生时间 和 最晚的完成时间

4、将图 G 的强连通分量 与 第一次深度优先搜索 所计算出的完成时间 关联起来
引理22.14 设 C 和 C’ 为有向图 G = (V, E) 的两个不同的强连通分量。假如存在一条边 (u, v) ∈ E,这里 u ∈ C,v ∈ C’,则 f(C) > f(C')

证明:根据深度优先搜索算法中 最先发现的结点在哪一个强连通分量里 分为两种情况进行考虑
第一种情况:如果 d(C) < d(C')
设 x 为连通分量 C 中 最早被发现的结点,那么 在时刻 x.d,所有 C 和 C’ 中的结点都是白色(d© < d(C’),意味着 C’ 发现得还要早)。在该时刻,图 G 包含一条 从结点 x 到 C 中每个结点的仅包含白色结点的路径。因为 (u, v) ∈ E,对于任意结点 w ∈ C’,在时刻 x.d 时,G 中也存在 一条从结点 x 到结点 w 的 仅包含白色结点的路径 x ⇝ u -> v ⇝ w
根据白色路径定理,连通分量 C 和 C’ 中的所有结点 都将成为 深度优先树里结点 x 的后代。根据 推论22.8,结点 x 的完成时间比其所有的后代都晚,因此 x.f = f(C) > f(C')

第二种情况:如果 d(C) > d(C')(跟第一种情况对称证明,不同的是 第一种情况都是后代,第二种情况 C 中还没有遍历到的,肯定更晚才能结束)
设 y 为 C’ 中最先被发现的结点,那么在时刻 y.d,所有 C’ 中的结点都是白色,且图 G 包含 一条从结点 y 到 C’ 中每个结点的仅包含白色结点的路径。根据白色路径定理,连通分量 C’ 中的所有结点 都将成为深度优先树里结点 y 的后代。根据 推论22.8,y.f = f(C’)。在时刻 y.d 时,连通分量 C 中的所有结点都是白色(d(C) > d(C')
由于存在边 (u, v) 将 C 连接到 C’,引理22.13 告诉我们,不可能存在一条从 C’ 到 C 的路径。因此,C 中的结点不可能从 y 到达。在时刻 y.f 时,所有 C 中的结点仍然是白色。因此,对于任意结点 w∈C 来说,有 w.f > y.f,这就意味着 f(C) > f(C')

5、根据上一条,可以推出 转置图 GT 中连接不同强连通分量的每条边 都是从完成时间较早(第一次深度优先搜索 所计算出的完成时间)的分量 指向 完成时间较迟的分量

推论22.15 设 C 和 C’ 为有向图 G = (V, E) 的两个不同的强连通分量,假如存在一条边 (u, v) ∈ ET(相当于 (v, u) ∈ E),这里 u∈C,v∈C’,则 f(C) < f(C'),即 GT 不可能包含 从 C 到任何其他强连通分量的边

6、推论22.15 是我们理解 为什么强连通分量算法能够 正确工作的关键。第二次深度优先搜索 运行在图 G 的转置图 GT 上。从完成时间最晚的强连通分量 C 开始。搜索算法从 C 中的某个结点 x 开始,访问 C 中的所有结点。根据 推论22.15,GT 不可能包含 从 C 到任何其他强连通分量的边

因此,从结点 x 开始的搜索 不会访问任何其他分量中的结点。因此,以 x 为根结点的树 仅包含 C 的所有结点。在完成对 C 中所有结点的访问后,算法第3行 从另一个强连通分量 C’ 选择一个结点作为根结点 来继续进行 深度优先搜索,这里 f(C’) 的取值在除 C 以外的所有强连通分量里面 为最大。再一次,搜索算法将访问 C’ 中的所有结点,但是根据 推论22.15,图 GT 中从 C’ 到任何其他连通分量的边 必定是从 C’ 到 C 的边,而这些边 已经访问过(在第一次搜索的时候就扫描完了,如果有的话 就不是两棵树了)

一般来说,当算法第3行对 GT 的深度优先搜索 访问任意一个强连通分量时,从该连通分量发出的所有边 只能是通向 已经被访问过的强连通分量。因此,每棵深度优先树 恰恰是一个强连通分量

7、定理22.16 算法 STRONGLY-CONNECTED-COMPONENTS 能够正确计算出 有向图 G 的强连通分量

证明:以算法第3行 对图 GT 进行深度优先搜索时 所发现的深度优先树的棵树 来进行归纳。归纳假设是,算法第3行所生成的前面 k 棵树 都是强连通分量。归纳证明的初始情况是 k = 0 时,归纳假设显然成立

在归纳步,假定算法第3行 所生成的前 k 棵树都是 强连通分量,现在需要考虑第 (k+1) 棵树。设该树的根结点为 u,结点 u 处于强连通分量 C 中。根据我们在算法第3行 选择深度优先搜素的 根结点的方式,对于任意除 C 以外且 尚未被访问的强连通分量 C’ 来说,有 u.f = f(C) > f(C')(注意是在 GT 中所以相反的,推论22.15)。根据归纳假设,在搜索算法 访问结点 u 的时刻,C 中的所有结点都是白色的。根据 白色路径定理,C 中的其他所有结点 都是结点 u 在深度优先树中的后代

而且,根据归纳假设 和 推论22.15,转置图 GT 中所有从 C 发出的边 只能是指向 已经访问过的强连通分量(根据 推论22.15,如果在转置图 GT 中存在从 C 发出的边 指向另一个强连通分量 C′,那么 f(C) < f(C′)(转置是反的)。由于 在算法第3行中按照完成时间 u.f 递减顺序 对顶点进行 DFS,所以 C 中的顶点 u 必须在 完成所有比 f(C) 大的 f(C′) 中的强连通分量 C′ 的 DFS 之后 才会被访问)。因此,除 C 以外的强连通分量中的结点 不可能在对 GT 进行深度优先搜索时 成为结点 u 的后代(即 不可能存在从 C 发出的边 指向另一个强连通分量 C′,不可能 f(C) < f(C′))。因此,转置图 GT 里根结点为结点 u 的深度优先树中的所有结点 恰好形成一个强连通分量

8、从另一个角度来查看 第二次深度优先搜索的运行过程。考虑转置图 GT 的分量图 (GT)SCC。如果将 第二次深度优先搜索 所访问的每个强连通分量 映射到 (GT)SCC 的一个结点上,则第二次深度优先搜索 将以拓扑排序顺序的逆序 来访问 (GT)SCC 中的结点。如果将 (GT)SCC 中的边翻转过来,将获得图 ((GT)SCC)T。因为 ((GT)SCC)T = GSCC,所以第二次深度优先搜索 是以拓扑排序顺序访问 GSCC 中的结点的(第一次是 以拓扑排序逆序访问 GSCC 中的结点)

练习22.5-1 如果在图 G 中加入一条新的边,G 中的强连通分量的数量 会发生怎样的变化

它(连通分量的数量)可以保持不变或减少。假设 将一条边添加到一个循环中。减少的情况是,假设 原始图 由三个顶点组成,并且这些顶点之间 仅仅是一条经过所有顶点 没有闭合 的路径,然后添加的边 将这条路径闭合成一个循环
为了说明它不能增加,注意到添加一条边 不能移除之前存在的任何路径(没有组件可以被拆分开)。因此,如果 u 和 v 在原始图中 属于同一个连通分量,那么 在这两个顶点之间 存在一条路径,且该路径 在两个方向上都存在。添加一条边不会破坏这两条路径,因此,在添加这条边之后,u 和 v 仍然在同一个强连通分量中

22.5-3 如果在第二次深度优先搜索中使用原图(而不是转置图)并按照完成时间递增的顺序扫描顶点,那么用于强连通分量的算法会更简单。这种简化的算法是否总能产生正确的结果

1)方向问题:在原图 G 中,边的方向可能会阻止某些顶点被访问,导致错误地认为 它们不属于同一个强连通分量
2)拓扑排序问题:按照完成时间递增的顺序访问顶点,实际上是在尝试按照分量图 GSCC 的拓扑排序的逆序访问顶点。这意味着 可能从一个强连通分量访问另一个强连通分量,而这两个分量之间实际上没有直接的路径(没办法保持 f(C) < f(C′)(这个大小本身有意义) 的性质,也就不可能使图 中所有从 C 发出的边 只能是指向 已经访问过的强连通分量,就是因为 是访问过的,所以已经输出了)

假设 图由三个顶点 {1, 2, 3} 组成,并且包含边 (2, 1)、(2, 3) 和 (3, 2)。那么,应该得到 {2, 3} 和 {1} 作为强连通分量 (SCCs)。然而,一个可能的从顶点2开始的深度优先搜索 (DFS) 可能会在1和3之前探索到3,这意味着3的完成时间 会低于1和2的完成时间。这意味着当第一次从顶点3开始执行DFS时,DFS将能够到达所有其他顶点。这意味着该算法会返回整个图作为一个单一的强连通分量 (SCC),但是显然不是这种情况,因为从1到2或从1到3之间没有路径

22.5-4
在这里插入图片描述
22.5-7 给定有向图 G = (V, E),如果对于所有结点对 u, v ∈ V,我们有 u ⇝ v 或 v ⇝ u,则 G 是半连通的。请给出一个有效的算法来判断图 G 是否是半连通的。证明算法的正确性并分析其运行时间

算法:
运行 STRONG-CONNECTED-COMPONENTS(G)
将每个强连通分量视为一个虚拟顶点,并创建一个新的虚拟图 G′
运行 TOPOLOGICAL-SORT(G′)
检查在 G′ 的拓扑排序中,所有连续的顶点对 (v_i, v_i+1) 是否都有边 (v_i, v_i+1) 存在于图 G′ 中。如果是,原图 G 是半连通图。否则,不是。

证明:
很容易证明 G′ 是一个有向无环图。考虑 G′ 中连续的顶点 v_i 和 v_i+1。如果没有从 v_i 到 v_i+1 的边,我们也可以得出 v_i 到 v_i+1 没有路径的结论,因为 v_i 在 v_i+1 之后完成。从 G′ 的定义,我们得出,在 G 中没有从表示为 v_i 的任何顶点到表示为 v_i+1 的顶点的路径。因此,G 不是半连通图。如果存在每对连续顶点之间的边,则我们可以断定在 G 中任何两个顶点之间都有路径。因此,G 是半连通图

运行时间:O(V + E)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值