程序中图的表示
邻接链表法
对于图G = (V, E),我们可以将之视为邻接链表的组合,该种表示方法在表示稀疏图(即边的条数|E|远远小于|V| ^ 2的图)时空间利用效率高而成为表示稀疏图,甚至是稠密图的主要方法之一。
对于上述例图,使用邻接链表的表示形式示意图如下:
不难从例子中总结出,对于图G = (V, E)来说,其邻接链表表示由一个包含|V|条链表的数组Adj组成,每个结点有一条链表。对于每一个结点u∈V,邻接链表Adj[u]包含所有与结点u之间有边相连的结点v,即Adj[u]包含图G中所有与u邻接的结点(也可以实现为链表中包含指向这些结点的指针)。
对于邻接链表表示而言,结点u的所有邻边的集合Adj[u]可以看做是图的一种属性。
邻接矩阵法
对于图G = (V, E),我们还可以将之表示为一个矩阵,该种表示方法在表示稀疏图时空间利用效率显然较低,但在表示稠密图时因为其实现形式较之邻接链表法难度显然降低,因此不失为一种好的选择。此外,邻接矩阵法的另一优势也体现在,对于任一边(u, v),邻接矩阵法表示可以在O(1)时间内确定(u, v)是否为图中的一条边。
对于之前的例子,我们可以用邻接矩阵法表示为以下的矩阵。
显然,对于一个图的邻接矩阵表示,若图中存在(u, v)的边,那么在矩阵的u行v列中值为1,反之,则为0。
表示图的属性
以上两种方法是在计算机中存储图的最为常见的两种形式。除此之外,我们当然可以自定义一种表示图的方法,只要这种方法可以表示出图的所有结点和所有邻边即可。显然,对于有向图,我们还需要指明边的方向。对于有权重的图,我们还需要表示出所有边的权重。
广度优先搜索
广度优先搜索是最基本的图算法之一,也是许多重要图算法的原型,因此学好广度优先搜索具有相当的意义。Prim的最小生成树算法和Dijkstra单源最短路径算法全部都使用了类似于广度优先搜索的算法思想。
可以解决那些问题?
给定一个图G = (V, E)和一个可以识别的源结点s,广度优先搜索对图G中的边进行系统性的探索来:
- 发现可以从源结点到达的所有结点
- 计算从源结点s到 每个 可到达的结点的距离(最少的边数)
- 生成一棵“广度优先搜索树”(树以源结点为根,包含所有可以从源结点s到达的结点v)
该算法既可以用于有向图,也可以运用于无向图。
概述
在初次理解广度优先算法时,我们可以先使用一种“染色”的方法方便我们理解,但实际上即使我们不“染色”,算法仍然可以正常的运作(此时我们需要使用其他的方法来标记某一结点是否被访问)。
正如前文所说,我们使用一种“染色”法来帮助理解,为了方便叙述,我们先行介绍一些这种方法下需要知道的内容,若在现在理解这些内容还难以接受,可以在之后阅读叙述时再翻阅回来。
- 初始化:当广度优先搜索算法开始时,我们将所有结点“染”为白色。
- 发现:从某一结点进行搜索时,发现了与之有邻边的白色结点,就称为“发现”这个结点,此时我们将这个结点染为“灰色”。
- 黑色结点:如果一个结点的所有边通向的结点都被“发现”,那我们就将这个结点染为“黑色”。
- 前驱/父结点:使一个“白色结点“被染为灰色的结点称之为白色结点的前驱。
接下来我们介绍广度优先搜索算法的流程,该算法的流程实际上就是构成一个广度优先搜索树的流程。首先,我们先初始化,然后将源节点加入搜索树(在将一任何一个结点加入搜索树时,我们视为“发现”了这个结点)。此时,搜索树种仅存在源节点。然后我们按发现顺序,扫描与结点u有有边的结点v,每当结点v是一个白色结点时,我们就将结点v也加入搜索树,并将它染为灰色。此时我们称结点u是结点v的前驱或者父结点。当结点u的所有边对应的结点都被“发现”或者结点u没有边或者结点u的边上没有可以被发现的结点时,我们将结点u染为黑色。当搜索树中所有结点都为黑色时,广度优先搜索算法结束。我们已经发现了源结点可以到达的所有结点,并完成了一个广度优先搜索树。
算法实现
虽然在算法概述中算法实现显得十分困难,然而在实现过程中,我们可以使用一个先入先出的队列来使我们的算法显得较为简明。可以用以下伪代码来表示:(伪代码中以vertex表示结点)
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 = ∅
Q.push(s)
while Q ≠ ∅:
u = Q.pop()
for each v ∈ G.Adj[u]:
if v.color == White then
v.color = Gray
v.d = u.d + 1
v.π = u
Q.push(v)
u.color = Black
注释:
结点的属性:我们在伪代码中认为每一个结点有以下一些属性:
color -> 结点的颜色,有White,Gray,Balck三种状态 d -> 从源节点到该节点的距离 π -> 该节点的前驱(父节点)
图的属性:正如之前在邻接链表中所提到的,我们在这里认为图的Adj[x]属性代表了结点x的所有邻边所连接的结点的集合。
下图是广度优先搜索在一个样例图上的推演过程:
伪代码的注释
在第2~5行中, 我们初始化了所有除过源结点以外的所有结点,显然的,在搜索未开始之前,除过源结点以外的所有结点都应是白色的,它与源节点的距离是不知道的,它的前驱(父结点)也是不知道的。
在第6~8行中,我们初始化了源结点,根据广度优先搜索的流程,源节点显然应该是灰色的,它与自身的距离为0,且因为他是第一个结点,他不存在前驱。
在第10~11行中,我们构造了一个搜索队列,并将源节点放入到搜索队列中。
在第12~20行中,我们构造了一个循环,我们每次从搜索队列中取出头部,并检测出与他有邻边的所有白色结点,在将其颜色染为灰色,设定好距离,并将前驱设为自身之后,也将这些子结点放入搜索队列中。在第20行时,一个结点的所有可能的子节点都已经被发现,因而将其染为黑色。
算法分析
我们在算法中保证了每个节点最多被入队或出队一次,每次入队或者出队的时间复杂度为O(1),因此对队列进行操作造成的时间复杂度为O(V)。同时算法中仍然保证了在使用邻接链表实现图时每条邻接链表最多被扫描一次,因而用于扫描邻接链表而造成的时间复杂度为O(E)。初始化操作造成了O(V)的时间复杂度。因此广度优先搜索的时间复杂度为O(V + E)。即广度优先搜索的运行时间是图G的邻接链表大小的一个线性函数。
深度优先搜索
深度优先搜索也是最基本的图算法之一。正如他的名字一样:只要可能,就在图中尽量的“深入”。
概述
对于深度优先搜索,我们也使用“染色”法进行处理。每个结点在初始状态时都为白色,在结点被发现后变为灰色,在其所有边都被探索完毕之后变为黑色。与广度优先搜索不同的是,我们在研究深度优先搜索时还在每个结点上盖上一个时间戳(实际上即使不使用时间戳或者染色的设定,算法仍然可以正常运行,此时需要使用其他的方法代替)。
深度优先搜索总是对最近发现的结点v的边进行探索,直到最新结点的已经没有边可以探索为止(或其所有边都已经被探索)。一旦最新发现的结点没有边可以探索,那么搜索则回溯到v的前驱(前驱的意义与广度优先搜索的前驱结点相似)结点,来搜索该前驱结点的边。
同广度优先搜索一样,我们仍然使用“染色”法来帮助理解,并且深度优先搜索的一些概念与广度优先搜索基本一致。
- 初始化:当深度优先搜索算法开始时,我们将所有结点“染”为白色。
- 发现:从某一结点进行搜索时,发现了与之有邻边的白色结点,就称为“发现”这个结点,此时我们将这个结点染为“灰色”。
- 黑色结点:如果一个结点的所有边通向的结点都被“发现”,那我们就将这个结点染为“黑色”。
- 前驱/父结点:使一个“白色结点“被染为灰色的结点称之为白色结点的前驱。
算法实现
在深度优先算法中,与广度优先算法不同,我们深度优先算法中结点的d属性与f属性具有特别的意义。我们将在之后的注释中提到,这里暂且不表,只是提醒你注意。
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 then
DFS-VISIT(G, u)
DFS-VISIT(G, u)
time = time + 1
u.d = time
u.color = Gray
for each v ∈ G.Adj[u]:
if v.color = White then
v.π = u
DFS-VISIT(G, v)
u.color = Black
time = time + 1
u.f = time
注释:
结点的属性:我们在伪代码中认为每一个结点有以下一些属性:
color -> 结点的颜色,有White,Gray,Balck三种状态 d -> 发现结点的时刻 f -> 搜索完毕结点所有边的时刻 π -> 该节点的前驱(父节点)
图的属性:正如之前在邻接链表中所提到的,我们在这里认为图的Adj[x]属性代表了结点x的所有邻边所连接的结点的集合。
伪代码的注释
DFS函数
在第2~4行中,进行初始化操作,将所有结点染为白色,并设置所有结点的前驱。
在第5行中,初始化时间计数器。
在第6~8行中,依次检查各个结点,当一个白色结点被发现时,则使用DFS-VISIT对结点进行访问。
DFS-VISIT函数
在第11行中,递增时间计数器。
在第12~13行中,设置被访问结点的发现时间并将其染为灰色。
在第14~17行中,构造了一个循环,试图访问结点的所有边。
在第18~20行中,将结点染为黑色,并设置计数器,设置结点的完成时间。
算法分析
显然,第2~4行和第6~8行的循环造成的时间复杂度为Θ(V),第14~17行的循环造成的时间复杂度为Θ(E),因而深度优先搜索的时间复杂度为Θ(V + E)。
思考
对于一下两种构造的图,使用两种图算法造成的空间复杂度会有什么区别?
- 所有其他结点都与中心结点连接,除此之外没有任何的边(如左图)
- 初始结点连接一个结点,之后所有的结点只与前一个节点连接(如右图)