原文来自《挑战程序设计竞赛》

深度优先搜索(DFS)和宽度优先搜索(BFS)都是常见的搜索算法。在学习DFS和BFS之前,我们首先得知道递归函数的概念。

1. 递归函数

通俗地讲,一个函数自己调用自己的行为就叫递归,该函数就叫递归函数。如计算一个数的阶乘,就可以利用递归来实现。
我们知道一个数的阶乘可以等于这个数乘上这个数减1的阶乘,如3!=3×2!3!=3\times2!3!=3×2!,便有递推式:

n!=n×(n−1)!n!=n\times(n-1)! n!=n×(n−1)!

规定0!=10!=10!=1,便可以很容易地编写出如下函数:

int f(int n) {if (n == 0) {return 1;}return n * f(n-1);
}

递归函数必须要有循环退出的条件,在这段代码中,n==0n==0n==0就是循环退出的条件。如果没有循环退出的条件,那么函数就会无限地调用下去,导致程序崩溃。
下面是计算阶乘的递归过程:

类似地,我们可以试着编写计算斐波那契数列的某一项的递归函数。斐波那契数列的定义如下:

设数列{an},\{a_n\},{an​},若满足a0=0a_0=0a0​=0,a1=1a_1=1a1​=1,an=an−1+an−2(n≥2)a_n=a_{n-1}+a_{n-2}(n\geq2)an​=an−1​+an−2​(n≥2),则称数列{an}\{a_n\}{an​}为斐波那契数列

根据定义,我们知道斐波那契数列的递推式为an=an−1+an−2a_n=a_{n-1}+a_{n-2}an​=an−1​+an−2​,循环退出的条件为n=0n=0n=0或n=1n=1n=1,这样就可以很容易地写出该递归函数:

int fib(int n) {if (n <= 1) {return n;}return fib(n-1) + fib(n-2);
}

我们在使用这个函数时,即使是求n=40n=40n=40这样nnn较小的情况的时候,也需要花费很长的时间。这是因为该递归函数在进行递归时,一次递归同时调用了两次自身,这样时间上就会随着nnn的增大指数级增加,如当n=5n=5n=5时:

可以看到有很多重复、不必要的调用,如fib(2)在程序中被调用了3次,这就浪费了时间,因此我们可以使用一个数组来将每一次计算得到的数存储起来,如果这一项被计算过了,直接返回数组对应的元素即可:

#include <bits/stdc++.h>
using namespace std;
#define max_N 10005
int arr[max_N];
int fib(int n) {if (n <= 1) {return n;}if (arr[n] != 0) {return arr[n];}return arr[n] = fib(n-1) + fib(n-2);
}
int main () {int n;cin >> n;int res = fib(n);cout << res;
}

2. 深度优先搜索

深度优先搜索(DFS,Depth-First Search)是搜索算法的一种,它从某一个状态开始,不断地转移状态直到无法转移,然后回退到前一步的状态,继续转移到其他状态,如此不断重复,直到找到最终的解。我们先来看一下深度优先搜索的搜索树:

从这个搜索树中可以看出,DFS是从根节点出发,每次遍历它的第一个孩子节点,当遍历到叶子节点时候,回退一步返回到它的父亲节点,接着遍历父亲节点的其它孩子节点。如此重复,直到遍历完所有节点。
我们来试着解一下部分和问题:

部分和问题:
给定整数a1a_1a1​,a2a_2a2​,…\dots…,ana_nan​,判断是否可以从中选出若干数,使得它们的和恰好等于kkk。
第一行输入一个整数nnn,表示有几个数据
第二行输入nnn个整数,表示aia_iai​
第三行输入一个整数,表示kkk
限制条件:
*1≤n≤201\leq n \leq 201≤n≤20
*−108≤ai≤108-10^8 \leq a_i \leq 10^8−108≤ai​≤108
*−108≤k≤108-10^8 \leq k \leq 10^8−108≤k≤108

样例输入1:
4
1 2 4 7
13
样例输出2:
Yes

样例输入2:
4
1 2 4 7
15
样例输出:
No

分析:
按照DFS的思想,我们可以把a1a_1a1​作为搜索树的根节点,左子树为a1a_1a1​被选用的情况,右子树为a1a_1a1​不被选用的情况,用一个变量sum来计算被选择数据的和。

#include <bits/stdc++.h>
using namespace std;
#define max_N 100000005
int a[max_N];
int n, k;
bool dfs (int i, int sum) {//循环退出的条件,当i==n时,只需要判断sum是否与k相等即可 if (i == n) {return sum == k;}//左子树,a[i]被选用的情况 if (dfs(i + 1, sum + a[i])) {return true;}//右子树,a[i]不被选用的情况 if (dfs(i + 1, sum)) {return true;}//不管选不选用a[i],都凑不成k,则返回false return false;
}
int main () {cin >> n;for (int i = 0; i < n; i++) {cin >> a[i];}cin >> k;if (dfs(0,0)) {cout << "Yes";} else {cout << "No";}return 0;
}

接着我们来提高一下难度:

Lakee Counting(POJ NO.2386):
有一个大小为N×MN\times MN×M的园子,雨后积起了水,八连通的积水被认为是连接在一起的,请求出园子里共有多少个水洼。(八连通指的是下图中相对www的∗*∗的部分)
∗∗∗***∗∗∗
∗w∗*w*∗w∗
∗∗∗***∗∗∗
第一行输入一个整数NNN
第二行输入一个整数MMM
接下来的NNN行MMM列表示园子的范围,其中“www”为积水
限制条件:
*N,M≤100N,M \leq 100N,M≤100

样例输入:
w........ww.w........ww.w........ww.
.www.....www.www.....www.www.....www
....ww...ww.....ww...ww.....ww...ww.
.........ww..........ww..........ww.
.........w...........w...........w..
..w......w....w......w....w......w..
.w.w.....ww..w.w.....ww..w.w.....ww.
w.w.w.....w.w.w.w.....w.w.w.w.....w.
.w.w......w..w.w......w..w.w......w.
..w.......w...w.......w...w.......w.
样例输出:
3

分析:
可以从任意一个www的位置入手,将这个位置用"."代替,并搜索它所对应八连通的位置,如果搜索的位置在园子内,并且值为www,则递归调用自身,重复上述步骤;直到某个点的八连通位置内没有www时退出循环。
依次对园子内的每个点进行如上操作,则dfs被调用的次数即为水洼的个数。

#include <bits/stdc++.h>
using namespace std;
#define max_N 105
int N,M;
char field[max_N][max_N];//园子
void dfs (int x, int y) {//将该位置用"."替换field[x][y] = '.';//循环遍历八连通的各个点for (int dx = -1; dx <= 1; dx++) {for (int dy = -1; dy <= 1; dy++) {int nx = x + dx, ny = y + dy;//判断该点是否在园子内并且是否为积水if (nx >= 0 && nx <= N && ny >= 0 && ny <= M && field[nx][ny] == 'w') {dfs(nx, ny);} }} return ;
}
void solve () {int res = 0;for (int i = 0; i < N; i++) {for (int j = 0; j < M; j++) {if (field[i][j] == 'w') {dfs(i, j);res++;}}}cout << res;
}
int main () {cin >> N >> M;for (int i = 0; i < N; i++) {for (int j = 0; j < M; j++) {cin >> field[i][j];}}solve();return 0;
}

3. 宽度优先优先搜索

宽度优先搜索(BFS,Breath-First Search)又称广度优先搜索,也是搜索算法的一种,它与深度优先搜索类似,从某个状态出发,搜索所有可恶意到达的状态。
与深度优先搜索的不同之处在于搜索的顺序,我们来看一下宽度优先搜索的搜索树:

可以看出宽度优先是先遍历根节点的所有孩子节点,再依次遍历每一个孩子节点的孩子节点。
其实深度优先搜索(DFS)隐式地利用了栈进行计算,而宽度优先搜索(BFS)利用了队列。搜索时首先将初始状态添加到队列里,然后从队列的最顶端不断取出状态,把从该状态可以转移到的状态中尚未访问过的部分加入队列;如此往复,直到队列为空或找到问题的解。深度优先搜索相当于对搜索树进行前序遍历,而宽度优先搜索则相当于层序遍历。

接下来我们来看一道经典的迷宫问题:

迷宫的最短路径:
给定一个大小为N×MN\times MN×M的迷宫。迷宫由通道和墙壁组成,每一步可以向邻接的上下左右四格的通道移动。请求出从起点到终点所需的最小步数。请注意,本题假定从起点一定可以移动到终点。
第一行输入一个整数NNN
第二行输入一个整数MMM
接下来的NNN行MMM列为迷宫矩阵,“#”表示墙壁,“.”表示通道,“S”表示起点,“G”表示终点
限制条件:
N,M≤100N,M \leq 100N,M≤100

样例输入:
10
10
#S######.#\#S\#\#\#\#\#\#.\##S######.#
......#..#......\#..\#......#..#
.#.##.##.#\#.\#\#.\#\#.\##.##.##.#
.#.........\#.........#........
##.##.####\#\#.\#\#.\#\#\#\###.##.####
....#....#....\#....\#....#....#
.#######.#.\#\#\#\#\#\#\#.\#.#######.#
....#.........\#.........#.....
.####.###..\#\#\#\#.\#\#\#..####.###.
....#...G#....\#...G\#....#...G#
样例输出:
22

宽度优先搜索按照距开始状态由近及远的顺序进行搜索,因此可以很容易地用来求最短路径、最少操作之类的问题的答案。这个问题中,状态仅仅是目前所在位置的坐标。因此可以构造pair或者编码成int来来表达状态。当状态更加复杂时,就需要封装成一个类来表示状态了。转移的方式为四方向移动,状态数与迷宫的大小是相等的,所以复杂度为O(4×N×M)=O(N×M)O(4\times N\times M)=O(N\times M)O(4×N×M)=O(N×M)。
只要将已经访问过的状态用标记管理起来,就可以很好地做到由近及远的搜索,这个问题要求最短距离,不妨用数组d[N][N]d[N][N]d[N][N]来把最短距离保存起来,初始时用一个充分大的常数INFINFINF来进行初始化,这样就保证了尚未到达的位置的值是INFINFINF,也就起到了标记的作用。
虽然到达终点时会停止搜索,可如果继续下去直到队列为空的话,就可以计算出各个位置的最短距离。此外,如果搜索到最后,ddd仍然为INFINFINF的话,这个位置就是无法从起点到达的。
用dx[4]dx[4]dx[4]和dy[4]dy[4]dy[4]两个数组来表示四个方向向量。这样通过一个循环可以实现方向的移动。

#include <bits/stdc++.h>
using namespace std;
#define max_N 20
const int INF = 100000000;char maze[max_N][max_N + 1];
int N, M;
int sx, sy; //起点
int px, py; //终点
int d[max_N][max_N];//4个方向移动的分量
int dx[4] = {1, 0, -1, 0};
int dy[4] = {0, 1, 0, -1}; //使用pair表示状态时,使用typedef会更加方便一些
typedef pair<int, int> P;int bfs () {queue<P> que;//初始化for (int i = 0; i < N; i++) {for (int j = 0; j < M; j++) {d[i][j] = INF;if (maze[i][j] == 'S') {sx = i;sy = j;}if (maze[i][j] == 'G') {px = i;py = j;}}} //起点加入队列que.push(P(sx, sy));d[sx][sy] = 0;//不断循环直到队列长度为0while (que.size()) {//从队列中取出第一个元素P p = que.front();que.pop();//如果这个点是终点,则结束搜索if (p.first == px && p.second == py) {break;} //四个方向的循环for (int i = 0; i < 4; i++) {int nx = p.first + dx[i];int ny = p.second + dy[i];//判断是否可以移动,以及是否已经被放问过if (nx > 0 && nx < N && ny > 0 && ny < M && maze[nx][ny] != '#' && d[nx][ny] == INF) {//如果可以移动,则讲该点加入到队列,并将起点到该点的距离确定为到p的距离+1que.push(P(nx, ny));d[nx][ny] = d[p.first][p.second] + 1; } } } return d[px][py];
}int main () {cin >> N >> M;for (int i = 0; i < N; i++) {for (int j = 0; j < M; j++) {cin >> maze[i][j];}}int res = bfs();cout << res;return 0;
}

运行的结果如下:

深度优先搜索和宽度优先搜索一样,都会生成所有能够遍历到的状态,因此需要对所有的状态进行处理时使用宽度优先搜索也是可以的。但是递归函数可以很简短地编写,而且状态的管理也更简单,所以大多数情况下还是用深度优先搜索实现。反之,在求取最短路时深度优先搜索需要反复经过同样的状态,所以此时还是使用宽度优先搜索为好。
宽度优先搜索会把状态逐个加入队列,因此通常需要与状态数成正比的内存空间。反之,深度优先搜索是与最大的递归深度成正比的。一般与状态数相比,递归的深度并不会太大,所以可以认为深度优先搜索更加节省内存。


这是博主写的第一篇博客,如有不明确的地方或者不懂的地方,欢迎在下方留言交流,博主也会继续分享算法知识

深度优先搜索(DFS)与广度优先搜索(BFS)详解相关推荐

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

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

  2. 一文搞定深度优先搜索(DFS)与广度优先搜索(BFS)【含完整源码】

    写在前面:博主是一位普普通通的19届双非软工在读生,平时最大的爱好就是听听歌,逛逛B站.博主很喜欢的一句话花开堪折直须折,莫待无花空折枝:博主的理解是头一次为人,就应该做自己想做的事,做自己不后悔的事 ...

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

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

  4. 【蓝桥杯】2015决赛A组 5 穿越雷区(深度优先搜索dfs、广度优先搜索bfs)

    历届试题 穿越雷区 问题描述 X星的坦克战车很奇怪,它必须交替地穿越正能量辐射区和负能量辐射区才能保持正常运转,否则将报废. 某坦克需要从A区到B区去(A,B区本身是安全区,没有正能量或负能量特征), ...

  5. 树的前序、中序、后序遍历及深度优先算法DFS、广度优先算法BFS及python实现

    刷Leetcode时遇到一种经典数据结构--树,树是典型递归思想来的,学习树有助于学习递归思想以及栈.队列(后续细说),本文对树的结构.遍历算法以及Python实现做总结,以供复习拓展 树是连通的无环 ...

  6. 【数据结构与算法】2.深度优先搜索DFS、广度优先搜索BFS

    原文链接:https://blog.csdn.net/qq_41681241/article/details/81432634 总结 一般来说,广搜常用于找单一的最短路线,或者是规模小的路径搜索,它的 ...

  7. 深度优先搜索(DFS)和广度优先搜索(BFS)探究

    附BFS解题代码: #include<iostream> #include<queue> using namespace std;char room[23][23]; int ...

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

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

  9. 数据结构-图的深度优先遍历(DFS)和广度优先遍历(BFS)算法分析

    https://www.cnblogs.com/qzhc/p/10291430.html 最后一个广度优先有错误,H不指向E,只有G指向E,所以顺序应该是ABCFDHGE

  10. 深度优先搜索遍历与广度优先搜索遍历

    分享一下我老师大神的人工智能教程!零基础,通俗易懂!http://blog.csdn.net/jiangjunshow 也欢迎大家转载本篇文章.分享知识,造福人民,实现我们中华民族伟大复兴! 深度优先 ...

最新文章

  1. 名人问题 算法 时间复杂度
  2. 【数据挖掘】神经网络 后向传播算法 向前传播输入 案例计算分析 ( 网络拓扑 | 输入层计算 | 隐藏层计算 | 输出层计算 )
  3. Java并发编程实战_一线大厂架构师整理:java并发编程实践教程
  4. php socket的一些问题
  5. Smart Template tile的绘制原理
  6. java实现人脸识别源码【含测试效果图】——DaoImpl层(BaseDaoUtilImpl)
  7. 颜宁:当科学家是幸福的
  8. electron 打印需要联网的吗?_英语四六级考试准考证怎么打印,需要彩打吗
  9. java 发送邮件_Spring Boot (十二):Spring Boot 邮件服务
  10. 第三章EF的基本使用 EF数据实体模型的更新
  11. Linux自学笔记——tcp wrapper
  12. android自定义对号,Android-手势识别(普通手势识别:上 下 左 右 ;自定义手势识别:对号,错号 等)...
  13. 离散数学:主范式(主析取范式、主合取范式)
  14. 项目经理论坛_项目经理晋升之路:诚信、自信、韧性
  15. React Native入门——布局实践:开发京东客户端首页(二)TabBar的构建
  16. 襄阳寻古2-襄阳城墙,铁佛寺
  17. 虚拟化开源技术有哪些_您使用哪些开源虚拟化工具?
  18. 服务器的系统日志路径,DirectAdmin 日志路径各种系统中查看方法Windows服务器操作系统 -电脑资料...
  19. Uniapp 应用消息通知插件 Ba-Notify
  20. ofo小黄跑路,中国人素质真的差吗?

热门文章

  1. ISSP使用详细步骤
  2. 02-结构型设计模式
  3. 【656】《普通生物学》1/2/3/5 基础/细胞结构通讯/分裂分化
  4. redis第九课-击穿,穿透,雪崩,redis作为分布式锁的可行性
  5. Pyecharts(v0.5.X)---简介
  6. 第十二周 小学生计算能力测试
  7. ubuntu 查看内存及cpu使用情况
  8. linux 下 apache-ftpserver的安装
  9. select2 不允许选项“data” 添加到select标签中
  10. stdout和stderr重定向