数据结构 | 第6章 深度优先搜索DFS

152 阅读2分钟

6.7 深度优先搜索

策略:

深度优先搜索()选取下一顶点的策略可概括为:优先选取最后一个被访问到的顶点的邻居。

于是,以顶点s为基点的DFS搜索,将首先访问顶点s;再从s所有尚未访问到的邻居中任取其一,并以之为基点,递归地执行DFS搜索。故各顶点被访问到的次序,类似于树的先序遍历(5.4.2节);而各顶点被访问完毕的次序,则类似于树的后序遍历(5.4.4节)。

实现:

 0001 template <typename Tv, typename Te> //深度优先搜索DFS算法(全图)
 0002 void Graph<Tv, Te>::dfs ( Rank s ) { //s < n
 0003    reset(); int clock = 0; Rank v = s; //初始化
 0004    do //逐一检查所有顶点
 0005       if ( UNDISCOVERED == status ( v ) ) //一旦遇到尚未发现的顶点
 0006          DFS ( v, clock ); //即从该顶点出发启动一次DFS
 0007    while ( s != ( v = ( ( v+1 ) % n ) ) ); //按序号检查,故不漏不重
 0008 }
 0009 
 0010 template <typename Tv, typename Te> //深度优先搜索DFS算法(单个连通域)
 0011 void Graph<Tv, Te>::DFS ( Rank v, int& clock ) { //v < n
 0012    dTime ( v ) = ++clock; status ( v ) = DISCOVERED; //发现当前顶点v
 0013    for ( Rank u = firstNbr ( v ); -1 < u; u = nextNbr ( v, u ) ) //枚举v的所有邻居u
 0014       switch ( status ( u ) ) { //并视其状态分别处理
 0015          case UNDISCOVERED: //u尚未发现,意味着支撑树可在此拓展
 0016             type ( v, u ) = TREE; parent ( u ) = v; DFS ( u, clock ); break;
 0017          case DISCOVERED: //u已被发现但尚未访问完毕,应属被后代指向的祖先
 0018             type ( v, u ) = BACKWARD; break;
 0019          default: //u已访问完毕(VISITED,有向图),则视承袭关系分为前向边或跨边
 0020             type ( v, u ) = ( dTime ( v ) < dTime ( u ) ) ? FORWARD : CROSS; break;
 0021       }
 0022    status ( v ) = VISITED; fTime ( v ) = ++clock; //至此,当前顶点v方告访问完毕
 0023 }

算法的实质功能,由子算法DFS()递归地完成。每一递归实例中,都先将当前节点v标记为DISCOVERED(已发现)状态,再逐一核对其各邻居u的状态并做相应处理。待其所有邻居均已处理完毕之后,将顶点v置为VISITED(访问完毕)状态,便可回溯。

若顶点u尚处于UNDISCOVERED(未发现)状态,则将边(v, u)归类为树边(tree edge),并将v记作u的父节点。此后,便可将u作为当前顶点,继续递归地遍历。

若顶点u处于DISCOVERED状态,则意味着在此处发现一个有向环路。此时,在DFS遍历树中u必为v的祖先(习题[6-13]),故应将边(v, u)归类为后向边(back edge)。

这里为每个顶点v都记录了被发现的和访问完成的时刻,对应的时间区间[dTime(v), fTime(v)]均称作v的活跃期(active duration)。实际上,任意顶点v和u之间是否存在祖先/后代的“血缘”关系,完全取决于二者的活跃期是否相互包含。

对于有向图,顶点u还可能处于VISITED状态。此时,只要比对v与u的活跃期,即可判定在DFS树中v是否为u的祖先。若是,则边(v, u)应归类为前向边(forward edge);否则,二者必然来自相互独立的两个分支,边(v, u)应归类为跨边(cross edge)。

DFS(s)返回后,所有访问过的顶点通过parent[]指针依次联接,从整体上给出了顶点s所属连通或可达分量的一棵遍历树,称作深度优先搜索树或DFS树(DFS tree)。与BFS搜索一样,此时若还有其它的连通或可达分量,则可以其中任何顶点为基点,再次启动DFS搜索。

最终,经各次DFS搜索生成的一系列DFS树,构成了DFS森林(DFS forest)。

实例:

image-20220714144936933.png

image-20220714145024691.png

最终结果如图(t)所示,为包含两棵DFS树的一个DFS森林。可以看出,选用不同的起始基点,生成的DFS树(森林)也可能各异。如本例中,若从D开始搜索,则DFS森林可能如图(u)所示。

图 6.9 以时间为 横 坐标, 绘 出 了 图 6.8(u)中DFS树内各顶点的活跃期。可以清晰地看出,活跃期相互包含的顶点,在DFS树中都是“祖先-后代”关系(比如B之于C,或者D之于F);反之亦然。

image-20220714145321132.png

复杂度:

空间:除了原图本身,深度优先搜索算法所使用的空间,主要消耗于各顶点的时间标签和状态标记,以及各边的分类标记,二者累计不超过O(n) + O(e) = O(n + e)。

时间:首先需要花费O(n + e)时间对所有顶点和边的状态复位。不计对子函数DFS()的调用,dfs()本身对所有顶点的枚举共需O(n)时间。不计DFS()之间相互的递归调用,每个顶点、每条边只在子函数DFS()的某一递归实例中耗费O(1)时间,故累计亦不过O(n + e)时间。综合而言,深度优先搜索算法也可在O(n + e)时间内完成。

应用:

深度优先搜索无疑是最为重要的图遍历算法。基于DFS的框架,可以导出和建立大量的图算法。

下面仅以拓扑排序为例,对DFS模式的应用做更为具体的介绍。

“开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 10 天,点击查看活动详情