算法导论20.3 Exercises 答案

20.3-1

如下是有向图的情形。

\(\begin{array}{|l|l|l|l|}\hline \texttt{FROM}\backslash \texttt{TO}&W&G&B\\ \hline W&\texttt{TBFC}&\texttt{BC}&\texttt{C}\\ \hline G&\texttt{TF}&\texttt{TFB}&\texttt{TFC}\\ \hline B&&\texttt{B}&\texttt{TBFC}\\ \hline \end{array}\)

其中,\(\texttt{T}\)表示树边,\(\texttt{B}\)表示后向边,\(\texttt{F}\)表示前向边,\(\texttt{C}\)表示横跨边。

如下是无向图的情形,去除了横跨边和前向边的情况。

\(\begin{array}{|l|l|l|l|}\hline \texttt{FROM}\backslash \texttt{TO}&W&G&B\\ \hline W&\texttt{TB}&\texttt{TB}&\\ \hline G&\texttt{TB}&\texttt{TB}&\texttt{TB}\\ \hline B&&\texttt{TB}&\texttt{TB}\\ \hline \end{array}\)

20.3-2

\(G\)中的每个节点\(v\)的发现时间\(v.d\)和完成时间\(v.f\)如下表所示:

\(\begin{array}{|l|l|l|}\hline v&v.d&v.f\\ \hline q&1&16\\ \hline r&17&20\\ \hline s&2&7\\ \hline t&8&15\\ \hline u&18&19\\ \hline v&3&6\\ \hline w&4&5\\ \hline x&9&12\\ \hline y&13&14\\ \hline z&10&11\\ \hline \end{array}\)

\(G\)中的每条边分类如下:

  • 树边: \((q, s),(s, v),(v, w),(q, t),(t, x),(x, z),(t, y),(r, u)\)
  • 后向边: \((w, s),(y, q),(z, x)\)
  • 前向边: \((q, w)\)
  • 横向边: \((u, y),(r, y)\)

20.3-3

\((u(v(y(xx)y)v)u)(w(zz)w)\)

20.3-4

点的颜色仅仅用于判断点的状态,只要当前节点已经被深度优先算法访问过了,那么当前点的颜色就没有用处了。因此黑色和灰色状态可以合并,与白色状态一起,我们就可以只使用\(1\)比特来表示点的状态。因此最终的做法是,将算法DFS-VISIT的第3行修改成u.color=BLACK,并将第8行删去。

20.3-5

a

充分性:无论是树边还是前向边,\(v\)都是\(u\)在DF树上的子节点。根据定理20.7的第三部分,有\(u.d<v.d<v.f<u.f\)

必要性:根据推论20.8,可以知道满足\(u.d<v.d<v.f<u.f\)时,必须满足\(v\)\(u\)的后代。此时边\((u,v)\)要么是树边,要么是前向边。

b

充分性:由于\((u,v)\)是后向边,因此\(u\)\(v\)的后代。因此,如果\(u=v\),那么\(v.d=u.d<u.f=v.f\),原结论成立。如果\(u\neq c\),那么根据定理20.7的第二部分,有\(v.d<u.d<u.f<v.f\)。因此原结论成立。

必要性:当\(u=v\)时,有\(v.d=u.d<u.f=v.f\)。因此\((u,v)\)是自环,而自环将会被归类到有向边中。当\(u\neq v\)时,有\(v.d<u.d<u.f<v.f\),此时根据定理20.7的第2部分,可以知道\(v\)\(u\)的祖先。因此边\((u,v)\)是后向边。

c

充分性:由于\((u,v)\)是横向边,因此\(u\)\(v\)并不互为祖孙关系,因此根据定理20.7的第一条,将会有\(u.d < u.f < v.d < v.f\)\(v.d < v.f < u.d < u.f\)这两种情况之一,我们将通过反证法证明前者是错误的。假设\(u.d< v.d\)成立,那么根据定理20.9,在时刻\(u.d\),将会有一条从\(u\)\(v\)的白色路径,那么此时\(v\)必定是\(u\)的后代,与横向边的定义不符合。因此必定有\(v.d < v.f < u.d < u.f\)

必要性:由于\(v.d < v.f < u.d < u.f\),根据定理20.7的第一部分,\(u,v\)并不就有祖孙关系,因此\((u,v)\)是横向边。

20.3-6

为了保证递归时上下文能够保存,我们设计非递归算法时,同样需要设计一个方法GET-NEXT-WHITE-VERTEX(G, u),它的功能是多次被调用时,返回邻接表G.Adj[u]的第一个白色节点(如果没有,那么返回NIL)。通过均摊分析,这个方法的平均时间复杂度可以达到\(O(1)\),此处我们不讨论GET-NEXT-WHITE-VERTEX的实现细节。

其余细节则和递归时的情形相近。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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)

// TOP(S)表示返回S的栈顶元素,POP(S)表示弹出S的栈顶元素。
DFS-VISIT'(G, u)
S = Ø
PUSH(S, u)
time = time + 1 // white vertex u has just been discovered
u.d = time
u.color = GRAY
while S ≠ Ø
u = TOP(S)
v = GET-NEXT-WHITE-VERTEX(G, u)
time = time + 1
if v == NIL
POP(S)
u.f = time
u.color = BLACK // blacken u; it is finished
else
v.π = u
v.d = time
v.color = GRAY
PUSH(S, v)

20.3-7

\(V=\{a,b,c\},E=\{(a,b),(b,a),(b,c)\},G=(V,E)\),那么从\(b\)\(c\)就会有路径\(b\rightarrow a\rightarrow c\)。但是如果按照从\(b\rightarrow a\rightarrow b \rightarrow c\)的顺序访问各个节点,那么就会有\(b.d=1,a.d=2,c.d=4\),并且树边集合为\(E_{\pi}=\{(b,a),(b,c)\}\),此时\(c\)不是\(a\)的后代。\(G\)如下图所示。

20.3-8

本例子重用题目20.3-7所提出来的图。如果按照从\(b\rightarrow a\rightarrow b \rightarrow c\)的顺序访问各个节点,由于从\(b\)\(c\)就会有路径\(b\rightarrow a\rightarrow c\),并且按照从\(b\rightarrow a\rightarrow b \rightarrow c\)的顺序访问各个节点,那么有\(c.d=4,a.f=3\),这是不满足条件\(c.d\le a.f\)的。

20.3-9

综合\(4\)种有向边的定义,可以设计出算法DFS''来打印有向图\(G\)中的每条有向边的种类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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''(G, u)
time = time + 1 // white vertex u has just been discovered
u.d = time
u.color = GRAY
for each vertex v in G.Adj[u] // explore each edge (u, v)
if v.color == WHITE
print (u, v) is a tree edge
v.π = u
DFS-VISIT''(G, v)
else if v.color == GRAY
print (u, v) is a back edge
else if u.d < v.d
print (u, v) is a forward edge
else
print (u, v) is a cross edge
time = time + 1
u.f = time
u.color = BLACK // blacken u; it is finished

如果\(G\)是一个无向图,那么直接删去判断前向边和横向边的判断即可,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
DFS-U(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-U(G, u, NIL)

// 第三个参数是为了避免父节点被作为子节点遍历。
DFS-VISIT-U(G, u, fa)
time = time + 1 // white vertex u has just been discovered
u.d = time
u.color = GRAY
for each vertex v in G.Adj[u] // explore each edge (u, v)
if v.color == WHITE
print (u, v) is a tree edge
v.π = u
DFS-VISIT-U(G, v, u)
else if v != fa and v.color == GRAY
print (u, v) is a back edge
time = time + 1
u.f = time
u.color = BLACK // blacken u; it is finished

20.3-10

如果一个节点\(u\)在一棵DF森林中是一个孤立节点,那么只需要做到以下要求即可:

  • \(u\)的所有后继节点都先被访问。
  • 接下来\(u\)被访问,那么\(u\)的所有出边都不是树边。
  • 接下来\(u\)的前驱节点被访问,由于\(u\)已经被访问过,因此所有\(u\)的入边都不可能是树边。

从而,\(u\)在这棵DF森林就是一个孤立节点,示意图如下所示。

其中,和\(u\)关联的所有节点旁边的数字表示它们被访问的其中一个相对时间戳。

20.3-11

无向图中的边仅仅有树边和后向边这两种,那么对树边,进行真实的遍历,并记录下来;而对于后向边,只需要往返跳跃一次即可,同样记下这个跳跃记录,算法由DFS-U'给出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
DFS-U'(G)
for each vertex u ∈ G.V
u.color = WHITE
u.π = NIL
select u ∈ G.V randomly
time = 0
let path be new array // path 表示一个全局变量,用来存储这条路径。
INSERT(path, u)
DFS-VISIT-U'(G, u)
return path

DFS-VISIT-U'(G, u)
time = time + 1 // white vertex u has just been discovered
u.d = time
u.color = GRAY
for each vertex v in G.Adj[u] // explore each edge (u, v)
if v.color == WHITE
INSERT(path, v) // 正向
v.π = u
DFS-VISIT-U'(G, v)
INSERT(path, u) // 反向
else
INSERT(path, v) // 正向
INSERT(path, u) // 反向
time = time + 1
u.f = time
u.color = BLACK // blacken u; it is finished

20.3-12

和原版dfs的方案类似,多维护一个全局变量component,用来表示当前遍历图\(G\)时的连通分量编号。这个值在遍历每一个连通分量之前就已经维护好,遍历过程中只需要赋值即可。算法由DFS-U''给出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
DFS-U''(G)
for each vertex u ∈ G.V
u.color = WHITE
u.π = NIL
time = 0
component = 0
for each vertex u ∈ G.V
if u.color == WHITE
component = component + 1
DFS-U''(G, u)

DFS-VISIT-U''(G, u)
time = time + 1 // white vertex u has just been discovered
u.d = time
u.color = GRAY
u.cc = component
for each vertex v in G.Adj[u] // explore each edge (u, v)
if v.color == WHITE
v.π = u
DFS-VISIT-U''(G, v)
time = time + 1
u.f = time
u.color = BLACK // blacken u; it is finished

\(\star\) 20.3-13

以下是一个比较暴力的做法,其期望时间复杂度为\(O(VE)\)

  1. 使用tarjan算法对图\(G\)进行缩点,得到\(G\)的所有边双连通分量,以及这些双连通分量之间的关系图\(G'\)。注意\(G'\)是一个有向无环图。这个步骤需要花费\(O(V+E)\)的时间。
  2. 枚举\(G\)中的所有双连通分量,判断这些双连通分量是否存在超过\(1\)个环。如果存在一个双连通分量超过\(1\)个环,那么\(G\)不是单连通图,算法结束。这个步骤需要花费\(O(V+E)\)的时间。
  3. \(G'\)中的每个入度为\(0\)的节点进向后进行搜索。在单次搜索中,如果存在一个节点\(v\)被访问了两次或以上,那么说明从\(u\)\(v\)有两条路径,原图\(G\)不是单连通图。否则\(G\)是单连通图。这个步骤需要花费\(O(VE)\)的时间。

因此,总共需要\(O(VE)\)的时间判断\(G\)是否为单连通图。