第22章-基本图算法

图的表示

\(G=(V, E)\),其中\(V\)表示结点,\(E\)表示边

  • 邻接链表: 表示稀疏图时,非常紧凑

    稀疏图:边的条数(\(|E|\))远远小于边的结点数(\(|V|\))的图

    • 需要空间\(\Theta(E+V)\)
    • 邻接表表示由一个包含\(|V|\)条链表的数组\(Adj\)所构成,对于结点\(u\in V\)\(Adj[u]\)表示图\(G\)中所有与\(u\)邻接的结点
  • 邻接矩阵:能快速查询两个结点之间是否有相连

    矩阵的行号表示起始点,列号表示终点

    • 需要空间\(\Theta(V^2)\)
    • 无向图的矩阵式一个对称矩阵,因为边\((u, v)\)和边\((v, u)\)是一样的

无向图的表示

有向图的表示

广度优先搜索

给定图\(G=(V, E)\)和一个源结点S,执行广度优先搜索的时候会生成一颗广度优先树。

对于结点\((u, v)\),在广度优先搜索树中,称结点\(u\)是结点\(v\)前驱或者父结点

最短路径距离: \(\delta(s, v)\)为从结点\(s\)到结点\(v\)之间所有路径里面最少的边数,如果\(s\)\(v\)之间没有路径相连,则\(\delta=\infty\)

总代价:\(O(E+V)\)

初始化操作的成本:O(V)

扫描邻接链表的时间:\(O(E)\)

算法为每个结点赋予了一点额外的属性, 以结点\(u\)为例

  • \(u.color\):结点的颜色
  • \(u.\pi\):结点前置结点
  • \(u.d\): 结点到源节点的距离
def BFS(G, s):
for each u in G.V - s:
u.color = WHITE
u.d = infinity
u.pi = NIL
s.color = GRAY
s.d = 0
s.pi = NIL
Q = 先进先出队列
ENQUEUE(Q, s)
while Q != NIL:
u = DEQUEUE(Q)
for each v in G[u]:
if v.COLOR = WHITE:
v.color = GRAY
v.d = u.d + 1
v.pi = u
ENQUEUE(v)
u.COLOR = BLACK

过程:

  1. 源头结点设置color=灰色,d=0, \(\pi\)=NIL, 其他结点color = 白色,d = 正无穷, \(\pi\)=NIL
  2. 创建一个FIFO队列,放入源结点
  3. 循环从队列中取结点\(u\)
    1. 迭代\(u\)的邻接列表,获得结点\(v\)
    2. 如果\(v\)的颜色是白色,讲其颜色改为灰色,然后距离+1,前置设置为\(u\)
    3. \(v\)放入队列
  4. \(u\)的颜色改为黑色(队列中取出的颜色都是灰色,所以这里都是将灰色->黑色)

深度优先搜索

广度优先搜索的前驱子图形成一颗树,而深度优先搜索的前驱子图可能由多棵树组成,因为搜索可能从多个源结点重复进行。

深度优先搜索的前驱子图,设图\(G_\pi = (V, E_\pi)\),其中\(E_\pi = {(v.\pi, v), v \in V 且 v.\pi \neq NIL}\)

所有的深度优先树是不相交的

深度优先搜索中结点的属性略有不同:

  • \(u.color\)
  • \(u.pi\)
  • \(u.d\):记录发现结点\(u\)的时刻
  • \(u.f\):记录完成结点\(u\)的时刻

步骤:

  1. 所有结点涂成白色, \(v.\pi=NIL\),时间time=0
  2. 循环图中每个结点\(u\), 如果结点颜色是白色:
    1. time=time+1, \(u\)的颜色变为灰色
    2. 循环G图中\(u\)结点邻接链表的每个结点\(v\), 如果\(v.color\)是白色,重复上面的操作
    3. \(u\)的颜色改为黑色,time=time+1, \(u\)设置完成时间
def DFS(G):
for each u in G.V:
u.color = WHITE
u.pi = NIL
time = 0
for each u in G.V:
if u.color == WHITE:
DFS_VISIT(G, u)

def DFS_VISIT(G, u):
time = time + 1
u.d = time
u.COLOR = GRAY
for each v in Adj[u]:
if v.color == WHITE:
DFS_VISIT(G, v)
u.color = BLACK
time = time + 1
u.f = time

深度优先探索的性质

  • 前驱子图\(G_\pi\)形成一个由多棵树所构成的森林
  • 结点的发现时间和完成时间具有所谓的括号化结构

边的分类

  • 树边:为深度森林\(G_\pi\)的边。如果结点\(v\)是因为算法对边\((u, v )\)的探索而首先呗发现,则\((u. v)\)是一条树边.
  • 后向边:后向边\((u, v)\)是将结点\(u\)连接到其在深度优先树中祖先结点\(v\)的边
  • 前向边:后向边\((u, v)\)是将结点\(u\)连接到其在深度优先树中后代结点\(v\)的边
  • 横向边:其他所有的边

拓补排序

对于一个有向无环图\(G=(V, E)\),来说,其拓补排序是\(G\)中所有结点的一种线性次序

def TOPOLOGICAL_SORT(G):
DFS(G)
# 根据G中结点的完成时间排序,从大到小
return sorted(G, lambda x: x.d, reverse=True)

强连通分量

深度搜索的经典应用:将有向图分解为强连通分量。

有向有环图中,环中的每个结点都可以确保访问。所以将环抽象为一个结点。

有向图\(G=(U, V)\)的强连通分量是一个最大结点集合\(C \subseteq U\), 对越该结点中任意两个结点\(u\), \(v\)来说,路径\(u\leadsto v\)和路径 \(v \leadsto u\)同时存在。

步骤:

  1. 使用DFS计算图G每个结点的结束时间
  2. 计算图\(G\)的转置\(G^T\)
  3. 使用DFS计算\(G^T\),但是在循环中,根据之前计算的每个结点的结束时间倒序。
  4. 将上一步获得的每一个深度搜索树返回,就是每个强连通分量。

练习

22.1-1

给定一个有向图的邻接表表示,计算该图中每个顶点的出度(发出的边的条数)需要多少时间?计算每个顶点的入度(进入的边的条数)需要多少时间?

\(O(|E|+|V|)\)

22.1-2

给出一个包含7个顶点的完全二叉树的邻接表表示,写出其等价的邻接矩阵表示。假设各个顶点如在一个二项堆中一样,从1到7进行编号。

因为完全二叉树

所以邻接链表: \[1 \rightarrow 2 \rightarrow 3 \] \[2 \rightarrow 1 \rightarrow 4 \rightarrow 5 \] \[3 \rightarrow 1 \rightarrow 6 \rightarrow 7 \] \[4 \rightarrow 2 \] \[5 \rightarrow 2 \] \[6 \rightarrow 3 \] \[7 \rightarrow 3\] 邻接矩阵:

1 2 3 4 5 6 7
1 0 1 1 0 0 0 0
2 1 0 0 1 1 0 0
3 1 0 0 0 0 1 1
4 0 1 0 0 0 0 0
5 0 1 0 0 0 0 0
6 0 0 1 0 0 0 0
7 0 0 1 0 0 0 0

22.1-3

# 链表
def transfer(G):
let G_T = new list of G^T
for u, v_list in |G.V|:
for v in v_list:
G_T[v].append(u)

# 矩阵
def transfer(G):
for i=1 to |G.V|-1:
for j=i+1 to |G.V|:
# 交换
G[j][i], G[i][j] = G[i][j], G[j][i]
return G

运行时间分别为:

  • \(O(|E|+|V|)\)
  • \(O(|V|^2)\)

22.1-4

给定一个多重图\(G=(V,E)\)的邻接表(多图是允许重复边和自循环边的图)表示,给出一个具有O(V+E)时间的算法,来计算其“等价”的无向图\(G'=(V,E')\)的邻接表表示,其中\(E'\)是将\(E\)中的冗余边和自循环边删除后余下的边。删除冗余边值得是将两个结点之间的多条边替换为一条边。

def clean_graph(G):
let G_new = new list of G
let A = [0]*|G.V|
for u in G.V:
for v in G[u]:
if v != u and u != A[v]:
A[v] = u
G_new[u].append(v)

22.1-5

有向图\(G=(V,E)\)平方图是图\(G^2=(V,E^2)\),这里,边\((u,v)\in E^2\)当且仅当图\(G\)包含一条最多由两条边构成的从\(u\)\(v\)的路径。请给出一个有效算法来计算图\(G\)的平方图\(G^2\).这里图\(G\)既可以以邻接链表表示,也可以以邻接矩阵表示,请分析算法的运行时间

答:

def square(G):
let G_square = new list of G
for u, Adj[u] in G:
for v in Adj[u]:
G_square[u].append(v)
for w in Adj[v]:
G_square[u].append(w)
return G_square

22.1-6

多数以邻接矩阵作为输入的图算法的运行时间为\(\Omega(V^2)\),但也有例外。给定图G的邻接矩阵表示,请给出一个\(O(V)\)时间的算法来判断有向图G是否存在一个通用汇点( universal sink)。通用汇点指的是入度为\(|V|-1\)但出度为0的结点

答:

在矩阵中从(1,1)开始寻找。如果值是1,就下一行(因为出度为0),如果是0,就检查转置的位置是否是1

def find_universal_sink(G):
# G是一个矩阵
i = j = 1
while i < |G.V| and j < |G.V|:
if G[i, j] == 1:
i++
elif G[i, j] == 0:
if G[j, i] == 1:
j++
else:
i++
return i

22.1-7

答: \[ B\cdot B^T(i, j)=\sum_{e\in E}{}b_{ie}b_{ej}^T = \sum_{e\in E}{}b_{ie}b_{je} \] 所以

  • 如果 i == j,则\(b_{ie}b_{je} = 1\)(当边\(e\)经过\(i,j\)的时候)或者0(边\(e\)不经过\(i, j\)
  • 如果 i != j,则\(b_{ie}b_{je}= -1\)(当点\(i, j\)是边\(e\)的两端的时候)或者0(点\(i, j\)不是边\(e\)的两端的时候)

所以 \[ B\cdot B^T(i, j) = \begin{cases} 经过i的边的数目, \quad i==j \\ -(连接i, j的边的数目), \quad i!=j \end{cases} \]

22.1-8

答:\(O(1)\)

22.2-1

请计算出在有向图22-2(a)上运行广度优先搜索算法后的\(d\)值和\(\pi\)值。这里假定结点3为算法所用的源结点

1 2 3 4 5 6
\(d\) \(\infty\) 3 0 2 1 1
\(\pi\) NIL 4 NIL 5 3 3

22.2-2

请计算出在图22-3所示无向图上运行广度优先搜索算法后的\(d\)值和\(\pi\)值。这里假定结点\(u\)为算法所用的源结点

\(r\) \(s\) \(t\) \(u\) \(v\) \(w\) \(x\) \(y\)
\(d\) 4 3 1 0 5 2 1 1
\(\pi\) \(s\) \(w\) \(u\) NIL \(r\) \(t\) \(u\) \(u\)

22.2-3

证明:使用单个位来存放每个结点的颜色即可。这个论点可以通过证明将算法第18行的伪代码删除后,BFS过程生成的结果不变来得到。

答:因为循环值判断颜色是不是白色,所以算法是可以删除18行(最后将完成得到结点变为黑色)

22.2-4

如果将输入的图用邻接矩阵来表示,并修改算法来应对此种形式的输入,请问BFS的运行时间将是多少?

答:初始化成本变高,从原来的\(O(E)\)升高到\(O(V^2)\),所以最后运行时间为\(O(V^2 + V)\)

22.2-5

证明:在广度优先搜索算法里,赋给结点\(u\)\(u.d\)值与结点在邻接链表里出现的次序无关。使用图22-3作为例子,证明:BFS所计算出的广度优先树可以因邻接链表中的次序不同而不同。

答:

22.2-6

举出一个有向图\(G=(V,E)\)的例子,对于源结点\(s∈V\)和一组树边\(E_\pi \in E\),使得对于每个结点\(v∈V\),图\((V,E_\pi)\)中从源结点\(s\)到结点\(v\)的唯一简单路径也是图G中的一条最短路径,但是,不管邻接链表里结点之间的次序如何,边集\(E_\pi\)都不能通过在图\(G\)上运行BFS来获得。

答:

22.2-7

职业摔跤手可以分为两种类型:“娃娃脸”(“好人”)型和“高跟鞋”(“坏人”)型。在任意对职业摔跤手之间都有可能存在竞争关系。假定有n个职业摔跤手,并且有一个给出竞争关系的r对摔跤手的链表。请给出一个时间为\(O(n+r)\)的算法来判断是否可以将某些摔跤手划分为“娃娃脸”型,而剩下的划分为“高跟鞋”型,使得所有的竞争关系均只存在于娃娃脸型和高跟鞋型选手之间。如果可以进行这种划分,则算法还应当生成一种这样的划分。

答:广度优先搜索,标记每个结点的颜色为"好人"或者“坏人”,然后交替连接好人坏人。时间:\(O(n+r) + O(r) = O(n + r)\)

22.2-8*

我们将一棵树\(T=(V,E)\)的直径定义为 \(max_{u,v\in V}\delta(u,v)\),也就是说,树中所有最短路径距离的最大值即为树的直径。请给出一个有效算法来计算树的直径,并分析算法的运行时间

答:

22.2-9

\(G=(V,E)\)为一个连通无向图。请给出一个\(O(V+E)\)时间的算法来计算图G中的条这样的路径:该路径正反向通过E中每条边恰好一次(该路径通过每条边两次,但这两次的方向相反)。如果给你大量的分币作为奖励,请描述如何在迷宫中找出一条路。

答:

def find(u):
"""
u是广度优先搜索的根结点
"""
for v in Adj[u] and Adj[v]是空:
go to v and back to u
for v in Adj[u] and v != u.pi:
go to v
find(v)
go to u.pi

22.3-1

答:

22.3-2

22.3-3

22.3-4

22.3-5

22.3-6

22.3-7

22.3-8

22.3-9

22.3-10

22.3-11

22.3-12

22.3-13

思考题

22-1

22-2

22-3

22-4