DFS(Depth First Search),深度优先遍历,是用于遍历或者搜索树或图的算法。深度优先则指的是,其每次搜寻都会尝试往更深结点走。
DFS在搜索算法中,常常利用函数递归实现暴力枚举,而DFS在图论中,则是对图的每个结点的遍历。
DFS最显著的特征在于其 递归调用自身 ,在遍历图时,对其访问的点打上访问标记,在遍历时跳过标记过的点,以确保每个点仅访问一次。
DFS大致结构如下:
DFS(v) // v 可以是图中的一个顶点,也可以是抽象的概念,如 dp 状态等。
在v上打上访问标记
for u in v的相邻结点
if u没有打过标记 then
DFS(u)
end
end
end
实际的DFS代码需要结合操作目的添加具体功能。
在数据结构中二叉树是一种重要的数据结构,其概念在此不赘述。这里用二叉树的DFS来举例DFS在图的遍历应用。
二叉树的DFS遍历主要有三种顺序:
前序:根左右
中序:左根右
后序:左右根
具体代码:
/*前序遍历:根左右*/
void printPreorder(struct node* node){
if (node == NULL){
return;
}
//遍历根结点 这里对遍历到节点的操作为输出值,具体问题可以改成其它操作
printf("%d ",node->data); //节点操作
//遍历左子树
printPreorder(node->left);
//遍历右子树
printPreorder(node->right);
}
/*中序遍历:左根右*/
void printInorder(struct node *node) {
if (node == NULL){
return;
}
//遍历左子树
printInorder(node->left);
//遍历根结点
printf("%d ",node->data);
//遍历右子树
printInorder(node->right);
}
/*后序遍历:左右根*/
void printPostorder(struct node* node)
{
if (node == NULL){
return;
}
//遍历左子树
printPostorder(node->left);
//遍历右子树
printPostorder(node->right);
//遍历根结点
printf("%d ", node->data);
}
DFS序,指的是在DFS调用过程中访问节点编号的序列。其记录了DFS遍历过程中各节点的访问顺序。这与上面二叉树的前中后序不同,前者是遍历序列,后者为遍历顺序。
该图的DFS序为: [ 1,2,3,6,4,5,7 ] 、 [ 1,5,7,4,2,6,3 ] 等。
同一颗树的 DFS序 可以不同,这取决于其遍历顺序,但并不影响DFS序的作用。
我们发现,因为DFS的访问特点,每一个子树都对应着DFS序列中连续的一段(一段区间)。
1 , [2 , 3 , 6] , 4 , 5 , 7 子树2
1 , 2 , 3 , 6 , 4 , [5 , 7 ] 子树5
证明: 在dfs遍历时,当进入一个节点之后,dfs会先把当前的节点的所有子节点都遍历一遍,然后在回溯到当前节点,在这个过程中,它的所有子孙节点一定都被访问过了,而且在这之前,它的任何一个子孙节点都不可能被访问过。然后它才离开当前子树,回到父亲节点,再访问其他子树,所以同一子树的节点在dfs序中一定是连续的一段。
这个性质使得在子树上进行的修改、查询可以转换成区间修改、区间查询。dfs序的主要作用就是将一个子树变成序列上的一个连续区间。
在DFS遍历时,顺便记录下访问顺序,例如设dfn[x]为第x个访问的节点。
如何获得当前节点在dfs序中的范围呢?
我们只要设一个时间 time,一开始是0,每遍历到一个新的节点就+1。那么,当前节点的访问时间范围就是当前节点的访问标号范围。设in[x]为进入当前节点时的时间,out[x]为离开当前节点时的时间,那么子树x在dfs序里所对应的范围就是in[x]~out[x]。
但是,我们在具体操作的过程中,往往不记录dfs序,因为重要的往往是范围。注意,in[x]是第一次访问节点x的时间,in[x]≠dfn[x],而dfn[in[x]]=x。
DFS(v)
在v上打上访问标记
dfn[cnt] = v
in[v] = cnt++
for u in v的相邻结点
if u没有打过标记 then
DFS(u)
end
end
out[v] = cnt++
end
欧拉序和DFS序差不多,不过欧拉序记录重复遍历到的节点。
其中一种欧拉序为:
[ 1 , 2 , 3 , 2 , 6 , 2 , 1 , 4 , 1 , 5 , 7 , 5 , 1 ]
在进出节点时都将其进行记录。其作用与dfs序差不多。可在求解LCA中发挥作用。
DFS(v)
在v上打上访问标记
O[cnt++] = v
for u in v的相邻结点
if u没有打过标记 then
DFS(u)
O[cnt++] = v
end
end
end
还有一种进入返回时加入一次:
[ 1 , 2 , 3 , 3 , 6 , 6 , 2 , 4 , 4 , 5 , 7 , 7 , 5 , 1 ]
DFS(v)
在v上打上访问标记
O[cnt++] = v
for u in v的相邻结点
if u没有打过标记 then
DFS(u)
end
end
O[cnt++] = v
end