深度优先搜索(DFS)

1.前言

深度优先搜索(缩写DFS)有点相似广度优先搜索,也是对一个连通图进行遍历的算法。它的思想是从一个顶点V0开始,沿着一条路一直走到底,若是发现不能到达目标解,那就返回到上一个节点,而后从另外一条路开始走到底,这种尽可能往深处走的概念便是深度优先的概念。算法

你能够跳过第二节先看第三节,:)数组

2.深度优先搜索VS广度优先搜索

 

2.1演示深度优先搜索的过程

仍是引用上篇文章的样例图,起点仍然是V0,咱们修改一下题目意思,只须要让你找出一条V0到V6的道路,而无需最短路。网络

 

图2-1 寻找V0到V6的一条路(无需最短路径)app

假设按照如下的顺序来搜索:函数

1.V0->V1->V4,此时到底尽头,仍然到不了V6,因而原路返回到V1去搜索其余路径;spa

2.返回到V1后既搜索V2,因而搜索路径是V0->V1->V2->V6,,找到目标节点,返回有解。.net

这样搜索只是2步就到达了,可是若是用BFS的话就须要多几步。blog

2.2深度与广度的比较

(你能够跳过这一节先看第三节,重点在第三节)递归

从上一篇《【算法入门】广度/宽度优先搜索(BFS) 》中知道,咱们搜索一个图是按照树的层次来搜索的。队列

咱们假设一个节点衍生出来的相邻节点平均的个数是N个,那么当起点开始搜索的时候,队列有一个节点,当起点拿出来后,把它相邻的节点放进去,那么队列就有N个节点,当下一层的搜索中再加入元素到队列的时候,节点数达到了N2,你能够想一想,一旦N是一个比较大的数的时候,这个树的层次又比较深,那这个队列就得须要很大的内存空间了。

因而广度优先搜索的缺点出来了:在树的层次较深&子节点数较多的状况下,消耗内存十分严重。广度优先搜索适用于节点的子节点数量很少,而且树的层次不会太深的状况。

那么深度优先就能够克服这个缺点,由于每次搜的过程,每一层只需维护一个节点。但回过头想一想,广度优先可以找到最短路径,那深度优先可否找到呢?深度优先的方法是一条路走到黑,那显然没法知道这条路是否是最短的,因此你还得继续走别的路去判断是不是最短路?

因而深度优先搜索的缺点也出来了:难以寻找最优解,仅仅只能寻找有解。其优势就是内存消耗小,克服了刚刚说的广度优先搜索的缺点。

3.深度优先搜索

 

3.1.举例

给出如图3-1所示的图,求图中的V0出发,是否存在一条路径长度为4的搜索路径。

 

图3-1

显然,咱们知道是有这样一个解的:V0->V3->V5->V6。

3.2.处理过程

 

3.3.对应例子的伪代码

这里先给出上边处理过程的对应伪代码。

 

[cpp] view plain copy

  1. /** 
  2.  * DFS核心伪代码 
  3.  * 前置条件是visit数组所有设置成false 
  4.  * @param n 当前开始搜索的节点 
  5.  * @param d 当前到达的深度,也便是路径长度 
  6.  * @return 是否有解 
  7.  */  
  8. bool DFS(Node n, int d){  
  9.     if (d == 4){//路径长度为返回true,表示这次搜索有解  
  10.         return true;  
  11.     }  
  12.   
  13.     for (Node nextNode in n){//遍历跟节点n相邻的节点nextNode,  
  14.         if (!visit[nextNode]){//未访问过的节点才能继续搜索  
  15.   
  16.             //例如搜索到V1了,那么V1要设置成已访问  
  17.             visit[nextNode] = true;  
  18.   
  19.             //接下来要从V1开始继续访问了,路径长度固然要加  
  20.   
  21.             if (DFS(nextNode, d+1)){//若是搜索出有解  
  22.                 //例如到了V6,找到解了,你必须一层一层递归的告诉上层已经找到解  
  23.                 return true;  
  24.             }  
  25.   
  26.             //从新设置成未访问,由于它有可能出如今下一次搜索的别的路径中  
  27.             visit[nextNode] = false;  
  28.   
  29.         }  
  30.         //到这里,发现本次搜索还没找到解,那就要从当前节点的下一个节点开始搜索。  
  31.     }  
  32.     return false;//本次搜索无解  
  33. }  


 

 

3.4.DFS函数的调用堆栈

 

此后堆栈调用返回到V0那一层,由于V1那一层也找不到跟V1的相邻未访问节点

 

此后堆栈调用返回到V3那一层

 

此后堆栈调用返回到主函数调用DFS(V0,0)的地方,由于已经找到解,无需再从别的节点去搜别的路径了。

4.核心代码

这里先给出DFS的核心代码。

 

[cpp] view plain copy

  1. /** 
  2.  * DFS核心伪代码 
  3.  * 前置条件是visit数组所有设置成false 
  4.  * @param n 当前开始搜索的节点 
  5.  * @param d 当前到达的深度 
  6.  * @return 是否有解 
  7.  */  
  8. bool DFS(Node n, int d){  
  9.     if (isEnd(n, d)){//一旦搜索深度到达一个结束状态,就返回true  
  10.         return true;  
  11.     }  
  12.   
  13.     for (Node nextNode in n){//遍历n相邻的节点nextNode  
  14.         if (!visit[nextNode]){//  
  15.             visit[nextNode] = true;//在下一步搜索中,nextNode不能再次出现  
  16.             if (DFS(nextNode, d+1)){//若是搜索出有解  
  17.                 //作些其余事情,例如记录结果深度等  
  18.                 return true;  
  19.             }  
  20.   
  21.             //从新设置成false,由于它有可能出如今下一次搜索的别的路径中  
  22.             visit[nextNode] = false;  
  23.         }  
  24.     }  
  25.     return false;//本次搜索无解  
  26. }  


 

 

固然了,这里的visit数组不必定是必须的,在一会我给出的24点例子中,咱们能够看到这点,这里visit的存在只是为了保证记录节点不被从新访问,也能够有其余方式来表达的,这里只给出核心思想。

深度优先搜索的算法须要你对递归有必定的认识,重要的思想就是:抽象!

能够从DFS函数里边看到,DFS里边永远只处理当前状态节点n,而不去关注它的下一个状态。

它经过把DFS方法抽象,整个逻辑就变得十分的清晰,这就是递归之美。

5.另外一个例子:24点

 

5.1.题目描述

想必你们都玩过一个游戏,叫作“24点”:给出4个整数,要求用加减乘除4个运算使其运算结果变成24,4个数字要不重复的用到计算中。

例如给出4个数:一、二、三、4。我能够用如下运算获得结果24:

1*2*3*4 = 24;2*3*4/1 = 24;(1+2+3)*4=24;……

如上,是有不少种组合方式使得他们变成24的,固然也有没法获得结果的4个数,例如:一、一、一、1。

如今我给你这样4个数,你能告诉我它们可以经过必定的运算组合以后变成24吗?这里我给出约束:数字之间的除法中不得出现小数,例如本来咱们能够1/4=0.25,可是这里的约束指定了这样操做是不合法的。

5.2.解法:搜索树

这里为了方便叙述,我假设如今只有3个数,只容许加法减法运算。我绘制了如图5-1的搜索树。

图5-1

 

此处只有3个数而且只有加减法,因此第二层的节点最多就6个,若是是给你4个数而且有加减乘除,那么第二层的节点就会比较多了,当延伸到第三层的时候节点数就比较多了,使用BFS的缺点就暴露了,须要很大的空间去维护那个队列。而你看这个搜索树,其实第一层是3个数,到了第二层就变成2个数了,也就是递归深度其实不会超过3层,因此采用DFS来作会更合理,平均效率要比BFS快(我没写代码验证过,读者自行验证)。

6.OJ题目

题目分类来自网络:

sicily:1019 1024 1034 1050 1052 1153 1171 1187

pku:1088 1176 1321 1416 1564 1753 2492 3083 3411

7.总结

DFS适合此类题目:给定初始状态跟目标状态,要求判断从初始状态到目标状态是否有解。

8.扩展

不知道你注意到没,在深度/广度搜索的过程当中,其实相邻节点的加入若是是有必定策略的话,对算法的效率是有很大影响的,你能够作一下简单马周游马周游这两个题,你就有所体会,你会发现你在搜索的过程当中,用必定策略去访问相邻节点会提高很大的效率。

这些运用到的贪心的思想,你能够再看看启发式搜索的算法,例如A*算法等。

相关文章
相关标签/搜索