前言

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


图的表示

对于图 G = (V,E),有两种标准表示方法。一个邻接表的集合和一个邻接矩阵,即邻接表表示(adiacent-list representation)和邻接矩阵表示adiacent-matrix representation)。它们每一个都可以表示有向图和无向图。

  • 对于稀疏图(sparse graph):当|E| « |V|2时通常用邻接表表示。
  • 对于稠密图(dense graph):|E| 与 |V|2相近。通常用邻接矩阵表示。

如下图,是对图(1)的邻接表(2)和邻接矩阵(3)的表示:


广度优先搜索(BFS)

广度优先搜索(breadth-first search)是最简单的图算法之一,也是许多重要的图算法原型,Prim最小生成树算法和Dijkstra单源最短路径算法都使用了类似广度优先搜索的思想。

给定一个图 G =(V,E)和一个源(source)点 s,广度优先搜索系统性地探索 G 的边去“发现”所有从源点出发可到达的顶点,并计算从源点到每一个可达(reachable)顶点v的距离,该距离等于从s到v经过的最少的边的数量。广度优先搜索生成一棵以s为根并包含所有可达顶点的“广度优先树”,树中从s 到v的简单路径对应从s到v的最路径。广度优先搜索算法既可以用于有向图,也可以用于无向图。

BFS的搜索过程是一层一层向外的,就犹如水波(wave)一样,例如:

简单来说,对于BFS的过程如下:

  1. 从s开始,一层一层地发现顶点。
  2. 首先访问距离s只有1条边的所有顶点(s的下一层)。
  3. 从那些顶点开始,访问距离下一层的所有顶点,以此类推。
  4. 直到它发现了从s出发可到达的每个顶点。

为此,我们还是用一个队列Q来记录需要访问的顶点。

下面给出广度优先搜索过程中,需要给每个顶点v添加额外属性:

  • v.color 表示的颜色: WHITE(该顶点未被发现)、GRAY(表示其邻接顶点可能还有未发现顶点)或BLACK(表示其邻接顶点全部被发现),初始所有顶点为白色。第一次发现某顶点,该顶点变为灰色,即该顶点被发现,所有灰色顶点构成搜索边界。当与某个顶点相连的所有边都被发现后,该顶点变为黑色。
  • v.d 表示从s到v的距离。
  • v.π 表示v的在广度优先树中的前驱,若v为源点或者未被发现,故v没有前驱,初始设v.π = NIL。

下面是BFS的伪代码:

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 = ∅
    ENQUEUE(Q, s)
    while Q ≠ ∅
        u = DEQUEUE(Q)
        for each vertex v in G.Adj[u]    // search the neighbors of u
            if v.color == WHITE    // is v being discovered now?
                v.color = GRAY
                v.d = u.d + 1
                v.π = u
                ENQUEUE(Q, v)    // v is now on the frontier
        u.color = BLACK

这个过程非常容易理解,我们通过一个例子来解释这个过程。

例如下图,我们把源点设为3,我们需要画的是BFS过程中每个节点的d值和π值,以及队列Q的出队顺序。

  1. 初始化情况下,我们把所有点的距离设置为∞,前驱π设置为NIL。
  2. 从3开始,我们先把3加入队列Q中,Q = 3。
  3. 对于3的下一层有两个节点5和6,把它们加入队列中(数字默认从小到大),并设置它们的距离d为1,前驱为3,3出队,于是现在就有了: Q = 5,6; 5.d = 6.d = 1; 5.π = 6.π = 3;
  4. 节点5出队,同时节点4入队,并设置4.d = 2, 4.π = 5,于是此时有Q = 6,4;
  5. 节点6出队,它没有下一层节点了于是无事发生,Q = 4;
  6. 节点4出队,同时节点2入队,并设置2.d = 3, 2.π = 4,于是此时有Q = 2;
  7. 节点2出队,它没有下一层节点了于是无事发生,队列Q为空,BFS结束;

最后可以得到这样的表:

1 2 3 4 5 6
v.d 3 0 2 1 1
v.π NIL 4 NIL 5 3 3

Q出队的顺序为:3,5,6,4,2

我们发现,节点1没有被BFS访问到,因为没有一个节点的下一层是它。它的d还是初始的∞,π还是NIL。

故:BFS不一定访问所有的节点


深度优先搜索(DFS)

深度优先搜索(depth-first search)优先沿着从最近发现的顶点v发出的一条边进行探索,尽管该顶点可能还有其他边未被探索。一旦从v发出的所有边都被探索后,则需要回溯(backtrack)到到达v的前一个顶点进行继续探索。不断执行上述过程,直到从源点可达的所有顶点全部被发现若还有顶点未被发现,选择未被发现的顶点中的任一顶点作为新的源点继续上述过程。深度优先搜索重复以上全部过程直到所有顶点全部被发现。

简单来说。DFS则是先探索一条道路到头,再回溯到分叉口进行其他探索

与BFS不同的是,DFS必须访问所有的顶点,所以可能深度优先搜索有多个源点。还有广度优先搜索的前驱子图是一棵树,但是深度优先搜索的前驱子图可能包含若干棵树,这是由于搜索可能从多个源点进行。

除了之前BFS的d外,深度优先搜索还给每个顶点加时间戳(timepstamp)。

每一个顶点v有两个时间戳:第一个时间戳v.d记录了v第一次被发现的时间(discovery time),此时变灰,第二个时间戳v.f 记录了v被搜索结束的时间(finish time),此时变黑。

故此时有如下算法:

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 discovered
    u.d = time
    u.color = GRAY
    for each vertex v ∈ G.Adj[u]    // explore each edge (u, v)
        if v.color == WHITE
            v.π = u
            DFS-VISIT(G, v)
    time = time + 1
    u.f = time
    u.color = BLACK    // blacken u; it is finished

我们同样通过一个例子来了解DFS的过程,设有如下的图,假设我们按照字典序建立邻接表:

  1. 按照字典序,先从u开始遍历,一路探索到头可以得到:u: 1/NIL, v: 2/NIL, y: 3/NIL, x: 4/NIL. 此时到了x已经无路可走了。
  2. 从x回溯到分叉口,我们发现这个路径(u->v->y->x)没有分叉口到新的节点,故一路回到u,此时:u: 1/8, v: 2/7, y: 3/6, x: 4/5
  3. 我们发现从u开始遍历的已经结束了,查看邻接表中,按照字典序,下一个没被遍历到的节点是w,于是从w开始继续遍历。
  4. 从w开始,发现只有z没被访问过,于是路径为w->z,有:w: 9/NIL, z: 10/NIL
  5. 此时同样到了z后无路可走了,于是原路返回有:w: 9/12, z: 10/11,至此我们所有的节点就访问完毕了。

最终图:

d/f表:

u v w x y z
d 1 2 9 4 3 10
f 8 7 12 5 6 11

后记

本章介绍了图的搜索算法,包括广度优先搜索和深度优先搜索。广度优先搜索通过按层次遍历图的节点,形成广度优先树,并记录节点的距离和前驱节点。深度优先搜索通过优先探索一条路径到底,然后回溯到分叉口进行其他探索,直到遍历所有节点。这两种搜索算法在图算法中具有重要的应用,如Prim最小生成树算法和Dikstra单源最短路径算法。通过掌握这些算法的原理和实现,可以更好地理解和解决各种图相关问题,提高算法效率。图的搜索是图算法领域的核心内容,对于进步学习和应用图算法具有重要意义。

这里的一切都有始有终,却能容纳所有的不期而遇和久别重逢。
最后更新于 2023-05-27