前言
图算法对于计算机学科至关重要。成百上千的计算问题最后都可以归约为图论问题。本章主要介绍图的表示和图的搜索。
图的表示
对于图G=(V,E)可以用两种标准表示方法表示:
- 邻接链表,因为在表示稀疏图(边的条数|E|远远小于 ∣ V ∣ 2 |V|^2 ∣V∣2的图)时非常紧凑而成为通常的选择。
- 邻接矩阵,在稠密图(|E|接近 ∣ V ∣ 2 |V|^2 ∣V∣2的图)的情况下,倾向于该表示法。
邻接链表表示由一个包含|V|条链表的数组Adj所构成,每个结点有一条链表。对于每个结点,邻接链表Adj[u]包含所有与结点u之间有边相连的结点v,即Adj[u]包含图G中所有与u邻接的结点。
- 邻接链表的表示法也有着潜在的不足之处是无法快速判断一条边(u, v)是否是图中的一条边。唯一的办法是在Adj[u]里面搜索v结点。邻接矩阵则客服的了这个缺陷,但是付出的代价是更大的存储空间。
- 对于无向图来说,邻接矩陈还有一个优势:第个记录项只需要1位的空间
广度优先搜索
对于图G,广度优先搜索算法需要在发现所有距离源结点s为k的所有结点之后,才会发现距离源结点s为k+1的其他结点。
# u.d记录的是广度优先搜索算法所计算出的从源点s到结点u之间的距离。
# u.π记录u的前驱结点。
BFS(G, s)
for each vertex u∈G.V - {s}
u.color = White
u.d = ∞
d.π = NIL
s.color = Gray
u.d = 0
d.π = 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
广度优先搜索算法可以得到从源顶点s到所有可达顶点的最短距离v.d。同时,该算法能够构造出广度优先树。下面的代码为打印一颗广度优先树中,源节点s到任意结点v的路径:
PRINT-PATH(G, s, v)
if v == s
print s
else if v.π == NIL
print “nopath from” s “to” v “exists”
else PRINT-PATH(G, s, v.π)
print v
深度优先搜索
对于图G,深度优先搜索总是对最近才发现的结点v的出发边进行探索,直到该结点的所有出发边都被发现为止。一旦结点v的所有出发边都被发现,搜索则“回溯”到v的前驱结点,来搜索前驱结点的出发边。
# u.d表示u结点开始搜索的时间
# u.f表示u结点结束搜索的时间
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 v∈G.Adj[u] // explore edge(u, v)
if v.color == White
v.π = u
DFS-Visit(G, v)
u.color = Black // black u, it is finished
time = time + 1
u.f = time
深度优先搜索算法的运行时间为θ(V+E)。
深度优先搜索的性质
深度优先搜索生成的前驱子图可能由多棵树组成,因为搜索可能从多个源结点重复进行。即深度优先森林。
深度优先搜索的一个重要特性是发现和完成时间具有括号结构。
图b是图a根据发现时间和结束时间的时序
对于图G,根据发现结速时间,总结三大性质:
- 性质1:在对有向图或无向图G = (V, E)进行的任意DFS过程中,对于任意两个节点u和v来说,下面三种情况只有一种成立:
- a:区间[u.d, u.f]和区间[v.d, v.f]完全分离,在深度优先森林中,节点u不是节点v的后代,节点v也不是节点u的后代。
- b:区间[u.d, u.f]包含在区间[v.d, v.f]中,在深度优先树中,节点u是节点v的后代。
- c:区间[v.d, v.f]包含在区间[u.d, u.f]中,在深度优先树中,节点v是节点u的后代。
- 性质二:在有向或无向图G的深度优先森林中,节点v是节点u的真后代当且仅当u.d < v.d <v.f < u.f成立。
- 性质三:在有向或无向图G = (V, E)的深度优先森林中,节点v是节点u的后代,当且仅当在时间u.d,存在一条从节点u到节点v的全部由白色节点所构成的路径。
边的分类
深度优先搜索的另一个有趣的性质是,可以通过搜索来对输入图G=(V, E)的边进行分类:
- 树边:为深度优先森林Gπ中的边。如果结点v是因算法对边(u, v)的探索而首先被发现,则(u, v)是一条树边。
- 前向边(上图灰色虚线):从某个点到它的某个子孙节点的边。这种边相当于提供某种“捷径”,在这个问题里不太重要,即使把它们全部删去,对于连通性也没什么影响。
- 后向边(上图绿色虚线):从某个点到它的某个祖先节点的边。这种边就是产生环的原因,如果删去所有反向边,那么原图会成为有向无环图。
- 横叉边(上图蓝色虚线):从某个点到一个既非它子孙节点、也非它祖先节点的边。这种边本身不产生环,但是它可能把两个强连通子图“连接”起来,形成一个更大的强连通子图。
反向边和横叉边都有一个特点:起点的dfs序必然大于终点的dfs序。
性质四:在对无向图G进行DFS时,每条边要么是树边,要么是后向边
拓扑排序
对于一个有向无环图G = (V,E)来说,其拓扑排序是G中所有结点的一种线性次序,该次序满足如下条件:如果图G包含边(u, v),则结点u在拓扑排序中处于结点v的前面。
拓扑排序就是将节点按完成时间进行降序排序
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
强连通分量
如果一个有向图中任意两个顶点互相可达,则称该有向图是强连通的。
有向图G=(V, E)的强连通分量是一个最大节点集合
C
∈
V
C\in V
C∈V,对于该集合中的任意一对节点u和v来说,路径u->v和路径v->u同时存在,即节点u和v可以相互抵达.
Kosaraju算法其实比较容易理解,它使用了两次DFS,算法的主要过程也就是这两次DFS:
- 第一次DFS计算完成时间的反序(实际上就是拓扑排序)。
- 第二次在图G转置图Gt上,按照第一次DFS计算出的顺序进行DFS。
主要利用“强连通分量”在转置图中,依旧是“强连通分量”。利用DFS分别计算图G与图Gt的连通性,然后取交集。
主要过程:《Kosaraju强连通子图算法》
主要参考
《Elementary Graph Algorithms》
《算法学习笔记(69): 强连通分量》
《Kosaraju强连通子图算法》
《强连通分量》