深度优先搜索(DFS)与广度优先搜索(BFS)详解

深度优先搜索(DFS)与广度优先搜索(BFS)详解深度优先搜索与宽度优先搜索详解深度优先搜索 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

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

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

1. 递归函数

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

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

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

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




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

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

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

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

我们在使用这个函数时,即使是求 n = 40 n=40 n=40这样 n n n较小的情况的时候,也需要花费很长的时间。这是因为该递归函数在进行递归时,一次递归同时调用了两次自身,这样时间上就会随着 n n n的增大指数级增加,如当 n = 5 n=5 n=5时:
fib(5)的递归过程
可以看到有很多重复、不必要的调用,如fib(2)在程序中被调用了3次,这就浪费了时间,因此我们可以使用一个数组来将每一次计算得到的数存储起来,如果这一项被计算过了,直接返回数组对应的元素即可:




#include  
     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. 深度优先搜索

部分和问题:
给定整数 a 1 a_1 a1, a 2 a_2 a2, … \dots , a n a_n an,判断是否可以从中选出若干数,使得它们的和恰好等于 k k k
第一行输入一个整数 n n n,表示有几个数据
第二行输入 n n n个整数,表示 a i a_i ai
第三行输入一个整数,表示 k k k
限制条件:
* 1 ≤ n ≤ 20 1\leq n \leq 20 1n20
* − 1 0 8 ≤ a i ≤ 1 0 8 -10^8 \leq a_i \leq 10^8 108ai108
* − 1 0 8 ≤ k ≤ 1 0 8 -10^8 \leq k \leq 10^8 108k108
















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










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










分析:
按照DFS的思想,我们可以把 a 1 a_1 a1作为搜索树的根节点,左子树为 a 1 a_1 a1被选用的情况,右子树为 a 1 a_1 a1不被选用的情况,用一个变量sum来计算被选择数据的和。
在这里插入图片描述




#include  
     using namespace std; #define max_N  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 × M N\times M N×M的园子,雨后积起了水,八连通的积水被认为是连接在一起的,请求出园子里共有多少个水洼。(八连通指的是下图中相对 w w w ∗ * 的部分)
∗ ∗ ∗ *
∗ w ∗ *w* w
∗ ∗ ∗ *
第一行输入一个整数 N N N
第二行输入一个整数 M M M
接下来的 N N N M M M列表示园子的范围,其中“ w w w”为积水
限制条件:
* N , M ≤ 100 N,M \leq 100 N,M100


















样例输入:
w . . . . . . . . w w . w……..ww. w……..ww.
. w w w . . . . . w w w .www…..www .www…..www
. . . . w w . . . w w . ….ww…ww. ….wwww.
. . . . . . . . . w w . ………ww. ………ww.
. . . . . . . . . w . . ………w.. ………w..
. . w . . . . . . w . . ..w……w.. ..w……w..
. w . w . . . . . w w . .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
























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




#include  
     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. 宽度优先优先搜索

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












样例输入:
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]来把最短距离保存起来,初始时用一个充分大的常数 I N F INF INF来进行初始化,这样就保证了尚未到达的位置的值是 I N F INF INF,也就起到了标记的作用。
虽然到达终点时会停止搜索,可如果继续下去直到队列为空的话,就可以计算出各个位置的最短距离。此外,如果搜索到最后, d d d仍然为 I N F INF INF的话,这个位置就是无法从起点到达的。
d x [ 4 ] dx[4] dx[4] d y [ 4 ] dy[4] dy[4]两个数组来表示四个方向向量。这样通过一个循环可以实现方向的移动。






#include  
     using namespace std; #define max_N 20 const int INF = ; 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; //不断循环直到队列长度为0 while (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的距离+1 que.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; } 

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

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/202693.html原文链接:https://javaforall.net

(0)
上一篇 2026年3月19日 下午11:31
下一篇 2026年3月19日 下午11:32


相关推荐

  • W25Q128FV译文(一)[通俗易懂]

    该文章包括W25Q128FV译文的前六章内容,第7章状态寄存器翻译及第八章指令部分翻译链接:https://blog.csdn.net/z123canghai/article/details/88726856第八章指令剩余部分及第九章相关时序翻译链接:https://blog.csdn.net/z123canghai/article/details/88726856目录一、概述…

    2022年4月4日
    489
  • Oracle修改表名报错ORA-14047

    Oracle修改表名报错ORA-140471、使用sys或其他用户修改表名SQL>showuser;USERis”SYS”SQL>altertableuser1.tb1renametouser1.tb2;ERRORatline1:ORA-14047:ALTERTABLE|INDEXRENAMEmaynotbecombinedwithotheroperations#使用非属主用户修改表名时修改后的表名不需要加属主正确修改方式:SQL>altertableuser

    2022年5月17日
    48
  • pycharm创建anaconda环境_conda怎么安装

    pycharm创建anaconda环境_conda怎么安装1、首先在condaprompt中创建新的环境。condacreate–name<env_name><package_names>尖括号代表文字内容,实际使用时不需要添加。如之后还需要再添加新的库进入环境,需在condaprompt中激活环境,并且利用pip安装新的包。<查看环境列表>condaenvlist或者condainfo-e<激活目标环境>activate<env_name>&l

    2022年8月25日
    9
  • 我部署了 OpenClaw,然后它崩溃了:一个真实用户的踩坑记录

    我部署了 OpenClaw,然后它崩溃了:一个真实用户的踩坑记录

    2026年3月13日
    2
  • 克莱姆法则应用_克莱姆和克拉默法则

    克莱姆法则应用_克莱姆和克拉默法则克莱姆法则(由线性方程组的系数确定方程组解的表达式)是线性代数中一个关于求解线性方程组的定理,它适用于变量和方程数目相等的线性方程组。概念含有n个未知数的线性方程组称为n元线性方程组。1)当其右端的常数项b1,b2,…,bn不全为零时,称为非齐次线性方程组:其中,A是线性方程组的系数矩阵,X是由未知数组成的列向量,β是由常数项组成的列向量。非齐次线性方程组的矩阵形式:2)当常数项全为零…

    2025年11月3日
    5
  • Windows 7 及 Vista无法启动MSN的解决办法 (转)

    Windows 7 及 Vista无法启动MSN的解决办法 (转)

    2021年5月7日
    143

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号