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)。
实例:
最终结果如图(t)所示,为包含两棵DFS树的一个DFS森林。可以看出,选用不同的起始基点,生成的DFS树(森林)也可能各异。如本例中,若从D开始搜索,则DFS森林可能如图(u)所示。
图 6.9 以时间为 横 坐标, 绘 出 了 图 6.8(u)中DFS树内各顶点的活跃期。可以清晰地看出,活跃期相互包含的顶点,在DFS树中都是“祖先-后代”关系(比如B之于C,或者D之于F);反之亦然。
复杂度:
空间:除了原图本身,深度优先搜索算法所使用的空间,主要消耗于各顶点的时间标签和状态标记,以及各边的分类标记,二者累计不超过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 天,点击查看活动详情”