图文详解两种算法:深度优先遍历(DFS)和广度优先遍历(BFS)

阅读本文前,请确保你已经掌握了递归、栈和队列的基本知识,如想掌握搜索的代码实现,请确保你能够用代码实现栈和队列的基本操作。

深度优先遍历(Depth First Search, 简称 DFS) 与广度优先遍历(Breath First Search,简称 BFS)是图论中两种非常重要的算法,也是进行更高的算法阶段学习的最后一道门槛。搜索算法频繁出现在算法竞赛题中, 尤其是深度优先搜索,在竞赛中,它是用来进行保底拿分的神器!

本文将会从以下几个方面来讲述深度优先遍历,广度优先遍历,相信大家看了肯定会有收获。

  • 什么是搜索?搜索用来干什么?

  • 深度优先遍历,广度优先遍历介绍

  • DFS vs BFS

  • 搜索的解题流程

  • 搜索中的常用术语

  • 搜索的一些优化

  • 习题演练

什么是搜索?搜索用来干什么?

搜索本质上就是枚举,只不过是一种有策略的枚举

搜索算法是利用计算机的高性能来有目的的穷举一个问题解空间的部分或所有的可能情况,从而求出问题的解的一种方法。现阶段一般有枚举算法、深度优先搜索、广度优先搜索、A*算法、回溯算法、蒙特卡洛树搜索、散列函数等算法。搜索本质上就是枚举,只不过是一种有策略的枚举, 通常在搜索前,根据条件降低搜索规模;根据问题的约束条件进行剪枝;利用搜索过程中的中间解,避免重复计算这几种方法进行优化 。

搜索算法实际上是根据 初始条件 和 扩展规则 构造一棵“解答树”并寻找符合目标状态的节点的过程。所有的搜索算法从最终的算法实现上来看,都可以划分成两个部分——控制结构(扩展节点的方式)和产生系统(扩展节点),而所有的算法优化和改进主要都是通过修改其控制结构来完成的。我们所熟悉的最常用的搜索算法:深度优先搜索和广度优先搜索就是有两种不同的控制结构(策略)的搜索算法。

其实,在这样的思考过程中,我们已经不知不觉地将一个具体的问题抽象成了一个模型——树,即搜索算法的使用 第一步在于搜索树的建立 。

深度优先遍历

深度优先遍历

主要思路是从图中一个未访问的顶点 V 开始,沿着一条路一直走到底,然后从这条路尽头的节点回退到上一个节点,再从另一条路开始走到底......,不断递归重复此过程,直到所有的顶点都遍历完成,它的特点是不撞南墙不回头,先走完一条路到底,再换一条路继续走。

树是图的一种特例(连通无环的图就是树),接下来我们来看看树用深度优先遍历该怎么遍历。

深搜过程图解

转存失败重新上传取消

1、我们从根节点 1 开始遍历,它相邻的节点有 2,3,4,先遍历节点 2,再遍历 2 的子节点 5,然后再遍历 5 的子节点 9。

转存失败重新上传取消

2、上图中一条路已经走到底了(9是叶子节点,再无可遍历的节点),此时就从 9 回退到上一个节点 5,看下节点 5 是否还有除 9 以外的节点,没有继续回退到 2,2 也没有除 5 以外的节点,回退到 1,1 有除 2 以外的节点 3,所以从节点 3 开始进行深度优先遍历,如下:

转存失败重新上传取消

3、同理从 10 开始往上回溯到 6, 6 没有除 10 以外的子节点,再往上回溯,发现 3 有除 6 以外的子点 7,所以此时会遍历 7。

转存失败重新上传取消

4、从 7 往上回溯到 3, 1,发现 1 还有节点 4 未遍历,所以此时沿着 4, 8 进行遍历,这样就遍历完成了。

完整的节点的遍历顺序如下(节点上的的蓝色数字代表):

转存失败重新上传取消

相信大家看到以上的遍历不难发现这就是树的前序遍历,实际上不管是前序遍历,还是中序遍历,亦或是后序遍历,都属于深度优先遍历。

那么深度优先遍历该怎么实现呢,有递归和非递归两种表现形式。

1、递归实现

递归实现比较简单,由于是前序遍历,所以我们先遍历当前节点,然后从左到右遍历当前节点的所有的子节点。在遍历每一个子节点的过程中,对于每一个子节点,依次遍历它们的所有子节点即可,依此不断递归下去,直到叶节点(递归终止条件)时返回,代码框架如下:

全局状态变量void dfs(当前状态)
{if(当前状态是目标状态)    // 判断进行相应处理(输出当前解、更新最优解、退出返回等)// 扩展for(所有可行的新状态)   {if(新状态没有访问过 && 需要访问)  // 可行性剪枝、最优性剪枝、重复性剪枝{标记dfs(新状态);取消标记}}
}int main()
{...dfs(初始状态);...
}

Copy

递归的表达性很好,也很容易理解,不过如果层级过深,很容易导致 栈溢出或超时 。原因有二:

(1)程序的运行时,函数的调用需要用到系统为函数申请一个函数栈(函数栈的概念),通俗点讲,这个栈的栈底就是我们熟悉的“main”函数,栈顶就是程序运行时当前所在的函数。如果递归调用的层级过深,就会导致这个函数栈需要保存的层数过多,而函数栈本身的容量是有限制的。因而可能会导致栈的溢出;

(2)考虑到上面的递归调用过程中,需要反复地将函数及相关信息压栈和入栈,而这个过程在反复执行多次后,其所耗费的时间已经不可以忽略了,因而容易导致程序超时。

下面讲解能有效避免栈溢出或超时问题的非递归实现。

2、非递归实现

仔细观察深度优先遍历的特点,对二叉树来说,由于是先序遍历(先遍历当前节点,再遍历左节点,再遍历右节点),所以我们有如下思路:

对于每个节点来说,先遍历当前节点,然后把右节点压栈,再压左节点(这样弹栈的时候会先拿到左节点遍历,符合深度优先遍历要求)。

弹栈,拿到栈顶的节点,如果节点不为空,重复步骤 1, 如果为空,结束遍历。

我们以以下二叉树为例来看下如何用栈来实现 DFS。

转存失败重新上传取消

整体动图如下:

转存失败重新上传取消
整体思路还是比较清晰的,使用栈来将要遍历的节点压栈,然后出栈后检查此节点是否还有未遍历的节点,有的话压栈,没有的话不断回溯(出栈),有了思路,不难写出如下用栈实现的深度优先遍历的代码框架:

全局状态变量
void dfs(初始状态)
{定义一个状态栈         // 用来保存搜索过程中的节点初始状态入栈           // 根节点入栈while(栈不为空){当前的状态 = 栈顶的状态(栈顶元素出栈)if(当前状态是目标状态)    // 判断进行相应处理(输出当前解、更新最优解、退出循环等)// 扩展for(所有可行的新状态)   {if(新状态没有访问过 && 需要访问) // 可行性剪枝、最优性剪枝、重复性剪枝{标记新状态入栈取消标记}}}
}int main()
{...dfs(初始状态);...
}

Copy

可以看到用栈实现深度优先遍历其实代码也不复杂(甚至和下面即将讲解的广搜的代码框架几乎一样),而且也不用担心递归那样层级过深导致的栈溢出问题。

广度优先遍历

广度优先遍历,指的是从图的一个未遍历的节点出发,先遍历这个节点的相邻节点,再依次遍历每个相邻节点的相邻节点。

上文所述树的广度优先遍历动图如下,每个节点的值即为它们的遍历顺序。所以广度优先遍历也叫层序遍历,先遍历第一层(节点 1),再遍历第二层(节点 2,3,4),第三层(5,6,7,8),第四层(9,10)。

转存失败重新上传取消

深度优先遍历用的是栈,而广度优先遍历要用队列来实现,我们以下图二叉树为例来看看如何用队列来实现广度优先遍历。

转存失败重新上传取消动图如下:

转存失败重新上传取消

相信看了以上动图,不难写出如下代码框架:

全局状态变量void BFS()
{定义状态队列初始状态入队while(队列不为空){取出队首状态作为当前状态if(当前状态是目标状态)进行相应处理(输出当前解、更新最优解、退出返回等)elsefor(所有可行的新状态)   {if(新状态没有访问过 && 需要访问)  // 可行性剪枝、最优性剪枝、重复性剪枝{新状态入队}}}
}

Copy

DFS vs BFS

对比我们可以发现:广度优先搜索和深度优先搜索(尤其是非递归的栈实现)的基本过程很相似,都包含如下两个过程:

  1. 判断边界:判断当前状态是否是目标状态,并进行相应处理

  2. 扩展新状态:由当前状态(节点)出发,扩展出新的状态(节点)

它们的区别就是一个用队列实现和一个用栈实现,一个按层横向遍历,一个按列纵深遍历。数据结构的不同也导致了它们对待 扩展出的新状态 的处理策略不同。深度优先搜索会将所有的新状态压入栈中,采取 先扩展出来的最后处理 的策略。而广度优先搜索会将所有的新状态压入队列中,采取 先扩展出来的先处理 的策略。这也正是栈 先入后出 和队列 先入先出 的体现。

搜索策略 优点 缺点 适用场景
深度优先 1.自动保留历史状态<br>2.函数参数即状态变量,状态定义简单<br>3.使用函数递归即可实现 1.递归非常耗时,容易TLE或爆栈<br>2.一般需要搜索完所有状态才能确定问题的解 1.需要求出所有解<br>2.需要输出状态路径
广度优先 1.求最优解时比DFS快 1.状态变量比较多时,实现麻烦 1.求最优解<br>2.状态定义简单时也可以用

需要注意的是:DFS(深度优先搜索)是一种相对更常见的算法,大部分的题目都可以用 DFS 解决(而BFS广搜就不行了),但是大部分情况下,这都是骗分算法,很少会有爆搜为正解的题目。因为 DFS 的时间复杂度特别高。

但是,如果想掌握更高级的算法,DFS必须要先熟练掌握!

搜索的解题步骤

  1. 读清题目,理清状态是什么,状态包含哪些要素,初始状态是什么,目标状态是什么;
  2. 建立搜索树,可以想象一下,或者在草稿纸上画,以帮助自己理解整个搜索树的构成,以及搜索的过程;
  3. 明确从每一个状态出发可以转移到哪些新的状态(通过题目中允许的操作进行转移),转移是如何影响状态的,哪些要素发生了变化?
  4. 需要进行回溯吗?状态的保存需要用全局变量吗?适合用广搜还是深搜?
  5. 可以进行哪些剪枝,以减少不必要的搜索?
  6. 按照代码框架,实现代码

搜索中的常用术语

状态空间:所有状态的集合

搜索树:从初始状态出发能访问的所有状态节点及对应路径构成的一棵树

状态:各种属性(位置、步数、和、路径记录[有时可以用全局变量记录])

初始状态:状态的各种属性为初始值

目标状态:一般是状态的某个属性满足一定条件下的状态,比如达到指定位置、和达到一定值

状态的转移:通过一些操作,导致了问题的状态发生了变化,就叫做状态的转移

可行解:所有目标状态都是可行解

最优解:目标状态中满足最优性(某属性最小、最大)的解,可能不唯一

解集:有时要求输出一个最优状态、有时要求输出所有的目标状态,有时要求输出任意一个目标状态

回溯:是一种经常被用在深度深度优先搜索(DFS)的技巧。其基本思想是——从一条路往前走,能进则进,不能进则退回来,换一条路再试。典型的例题是:八皇后问题。

方向数组:在二维数组上的搜索会经常用到的,用来更方便地实现状态转移和扩展的数组。常见的有四方向数组、八方向数组。

// 四方向数组的一种写法
int dx[4] = {0,0,-1,1};  // 左、右、上、下
int dy[4] = {-1,1,0,0};

Copy

搜索的一些优化

剪枝

剪枝,顾名思义,就是通过一些判断,砍掉搜索树上不必要的子树。有时候,我们会发现某个结点对应的子树的状态都不是我们要的结果,那么我们其实没必要对这个分支进行搜索,砍掉这个子树,就是剪枝。

最常用的剪枝有三种,可行性剪枝、重复性剪枝、最优性剪枝。

可行性剪枝:在搜索过程中,一旦发现如果某些状态无论如何都不能找到最终的解,就可以将其“剪枝”了,比如越界操作、非法操作。一般通过条件判断来实现,如果新的状态节点是非法的,则不扩展该节点

重复性剪枝:对于某一些特定的搜索方式,一个方案可能会被搜索很多次,这样是没必要的。在实现上,一般通过一个记忆数组来记录搜索到目前为止哪些状态已经被搜过了,然后在搜索过程中,如果新的状态已经被搜过了,则不再扩展该状态节点。

最优性剪枝:对于求最优解的一类问题,通常可以用最优性剪枝,比如在求解迷宫最短路的时候,如果发现当前的步数已经超过了当前最优解,那从当前状态开始的搜索都是多余的,因为这样搜索下去永远都搜不到更优的解。通过这样的剪枝,可以省去大量冗余的计算,避免超时。在实现上,一般通过一个记忆数组来记录搜索到目前为止的最优解,然后在搜索过程中,如果新的状态已经不可能是最优解了,那再往下搜索肯定搜不到最优解,于是不再扩展该状态节点。

转存失败重新上传取消

其他的剪枝策略:

奇偶性剪枝

我们先来看一道题目:有一个n×m大小的迷宫。其中字符S表示起点,字符D表示出口,字符X表示墙壁,字符.表示平地。你需要从S出发走到D,每次只能向上下左右相邻的位置移动,并且不能走出地图,也不能走进墙壁。每次移动消耗1时间,走过路都会塌陷,因此不能走回头路或者原地不动。现在已知出口的大门会在T时间打开,判断在0时间从起点出发能否逃离迷宫。数据范围n,m≤10,T≤50。

我们只需要用DFS来搜索每条路线,并且只需搜到T时间就可以了(这是一个可行性剪枝)。但是仅仅这样也无法通过本题,还需考虑更多的剪枝。

转存失败重新上传取消

如上图所示,将n×m的网格染成黑白两色。我们记每个格子的行数和列数之和x,如果x为偶数,那么格子就是白色,反之奇数时为黑色。容易发现相邻的两个格子的颜色肯定不一样,也就是说每走一步颜色都会不一样。更普遍的结论是:走奇数步会改变颜色,走偶数步颜色不变。

那么如果起点和终点的颜色一样,而T是奇数的话,就不可能逃离迷宫。同理,如果起点和终点的颜色不一样,而T是偶数的话,也不能逃离迷宫。遇到这两种情况时,就不用进行DFS了,直接输出"NO"。

这样的剪枝就是奇偶性剪枝,本质上也属于可行性剪枝。

习题演练

参见训练中的章节内题目,值得一提的是:

第一章除最后一题是搜索的入门题外,其他都是递推和递归类的题目,建议用递推和递归都各实现一遍,以复习相应的知识点;

第三章的细胞既可以用深搜做,也可以用广搜做,建议两种写法各实现一遍,以加深理解。其实所有的题目都可以尝试用两种方法去解决看一看,以体会深搜和广搜在实现上的不同,以及它们不同的适用场合;

第五章的N皇后问题是回溯法的经典问题,而且需要进行剪枝,建议认真做;

第五章的数谜这道题考察了深搜的一些技巧,而且需要进行深度优化才能通过,值得认真去做,做完可以看题解区的题解总结;

图文详解两种算法:深度优先遍历(DFS)和广度优先遍历(BFS)相关推荐

  1. JavaScript(JS)前序遍历,中序遍历,后序遍历,层序遍历,图文详解两种(递归与迭代)实现的方式

    1.二叉树的前序遍历 前序遍历首先访问根结点然后遍历左子树,最后遍历右子树. 在遍历左.右子树时,仍然先访问根结点,然后遍历左子树,最后遍历右子树. 若二叉树为空则结束返回,否则: (1)访问根结点. ...

  2. 数据结构与算法(7-2)图的遍历(深度优先遍历DFS、广度优先遍历BFS)(分别用邻接矩阵和邻接表实现)

    目录 深度优先遍历(DFS)和广度优先遍历(BFS)原理 1.自己的原理图 2.官方原理图 一.邻接矩阵的深度优先遍历(DFS) 1.原理图 2. 过程: 3.总代码 二.邻接表的深度优先遍历(DFS ...

  3. 图文详解 23 种设计模式

    一直想写一篇介绍设计模式的文章,让读者可以很快看完,而且一看就懂,看懂就会用,同时不会将各个模式搞混.自认为本文还是写得不错的,花了不少心思来写这文章和做图,力求让读者真的能看着简单同时有所收获. 设 ...

  4. 7-1 寻找大富翁 (25 分)(思路加详解+两种做法(一种优先队列,一种vector容器))

    一:题目 胡润研究院的调查显示,截至2017年底,中国个人资产超过1亿元的高净值人群达15万人.假设给出N个人的个人资产值,请快速找出资产排前M位的大富翁. 输入格式: 输入首先给出两个正整数N(≤1 ...

  5. 详解两种C#自动实现DLL(OCX)控件注册的方法

    本文将为大家讲述DLL库自动注册的两种方法,包括调用Regsvr32方法等.希望通过本文能对大家有所帮助. 尽管MS为我们提供了丰富的.NET Framework库,我们的程序C#开发带来了极大的便利 ...

  6. 图片轮播的实现(详解两种方法)

    今天带来的是前端里图片轮播的实现,可以说,这两种方法都很简单,尤其第一种,只要是有点基础的应该都可以看懂,这也是小编花费了一定时间想到的代码较少的方式.(图片我放在文末了) 当然也有更复杂的图片,也会 ...

  7. 235、一个带宽如何连接两个路由器?详解两种方法

    于一个网络如何连接两个路由器?家里房间比较多,之前已经安装了一个无线路由器,但是在某些房间信号很差,想再增加一个无线路由器怎么办? 实现一个带宽连接两个路由器,有两种方法: 1.二级路由器 2.无线桥 ...

  8. 图的遍历(深度优先遍历DFS,广度优先遍历BFS)以及C语言的实现

    遍历的定义: 从已给的连通图中某一顶点出发,沿着一些边访遍图中所有的顶点,且使每个顶点仅被访问一次,就叫做图的遍历,它是图的基本运算. 一:深度优先遍历(DFS) 1,在访问图中某一起始顶点V后,由V ...

  9. 图的深度优先搜索(DFS)和广度优先搜索(BFS)算法

    深度优先(DFS) 深度优先遍历,从初始访问结点出发,我们知道初始访问结点可能有多个邻接结点,深度优先遍历的策略就是首先访问第一个邻接结点,然后再以这个被访问的邻接结点作为初始结点,访问它的第一个邻接 ...

最新文章

  1. html class css,div id class
  2. myeclipse新建映射文件xxx.hbm.xml
  3. [转载]通过Arcgis Server向MXD中添加图层
  4. 在caffe上fine-tuning网络
  5. maskrcnn还可以加网络吗_绿茶加蜂蜜的功效,绿茶可以加蜂蜜吗?
  6. etf基金代码大全_银行ETF最新规模首超28亿元再创历史新高,近4个月资金净流入超12亿元...
  7. NY : 括号匹配问题
  8. mnist数据集svm python_python支持向量机分类MNIST数据集
  9. 招了一大群学生的游戏代码
  10. 视觉SLAM——2D-2D:对极几何
  11. Spring中事务使用
  12. pytorch保存.pth文件
  13. the database profile could not loaded. Check log for details
  14. Spotfire 修改标记及颜色
  15. 在CentOS8.3上安装Vlmcsd-1113搭建Kms服务
  16. numpy的随机抽样
  17. 图像空间域分析之图像统计特征
  18. 做到这3点,你也能成为一个高情商的人
  19. 基于ChatGPT制作的一款英语口语练习应用SpokenAi
  20. 锐龙R3 2200G和Intel i3-8100选哪个好

热门文章

  1. npm 淘宝||华为-镜像的安装(2022最新版)
  2. Matlab 点云旋转之四元数
  3. 摄影 闪光灯同步(前帘同步,后帘同步,后期堆栈)
  4. 10 python程序设计:试卷生成与分析
  5. 【SWH模型】陆地生态系统蒸散模拟理论、蒸散估算、站点及区域尺度模拟
  6. [Jenkins] Failed to start Jenkins Continuous Integration Server
  7. $translate 的用法
  8. C++标准模板库(STL)笔记与示例
  9. 创建获奖场景平面设计
  10. 给 GitHub commit 加个小绿标