0
点赞
收藏
分享

微信扫一扫

算法学习笔记 4-3 深搜(DFS)与广搜(BFS):初识问题状态空间与 LeetCode真题图解(Java)

倚然君 2022-05-03 阅读 81

4-3 深搜(DFS)与广搜(BFS):初识问题状态空间

搜索的核心概念

首先给大家拓展一个概念,这个概念就是我们学习搜索算法中非常重要的一环:

image-20220224130200468

这个问题求解树是一个抽象的概念,是在我们自己脑海中根据问题所展开的一个树,也叫递归搜索树,状态展开树。

现在大家对这个树肯定还是一片朦胧,我们根据具体的例子来讲解一下:

( 0 , 0 ) (0, 0) (0,0)点有一头🐮,它需要找到在 ( 3 , 3 ) (3, 3) (3,3)的⭐️,* 是障碍物。

image-20220224130726022

怎么根据这个走迷宫对应我们的问题求解树呢?我们的初始状态1就是🐮所在的坐标,🐮可以往下走或者往右走,往下走坐标变为 ( 1 , 0 ) (1, 0) (1,0),往右走坐标变为 ( 0 , 1 ) (0, 1) (0,1)

image-20220224131432409

再展开一部分:

image-20220224131515867

这样逐步的展开,我们一定能寻找到 ( 3 , 3 ) (3, 3) (3,3)⭐️的位置。

image-20220224175834544

如果走迷宫你不会走,那我们就把问题转换成在二叉树上的遍历问题。

什么是深搜和广搜?

我们深搜和广搜分别代表着我们遍历问题求解树不同的形式。

什么是搜索剪枝和优化?

我们把多余的树枝剪掉,此时 ( 1 , 1 ) (1,1) (1,1)这个点无论是往下走还是往右走都会遇到 *,所以我们可以直接将 ( 1 , 1 ) (1,1) (1,1)这个分支去掉,就做到了优化。

image-20220326191655057

设计搜索算法的核心关键点是什么?

搜索算法最核心的问题:设计我们问题求解树当中节点的状态。

因为我们搜索就是相当于在整个问题求解树中遍历。

我们如何设计问题求解树中每一个节点代表的意义,就是我们搜索算法的核心关键点。

DFS — 深度优先搜索

我们从起点开始搜索,直到搜索到边界我们再返回到上一节点:

image-20220326192841380

此时回到 ( 0 , 1 ) (0,1) (0,1)节点,又搜索到了 ( 1 , 1 ) (1,1) (1,1)节点:

image-20220326193116685

( 1 , 1 ) (1,1) (1,1)节点也是边界了,此时我们左子树已全部搜索,所以我们返回到起点,开始搜索右子树,与搜索左子树同理。

其实我们发现,深度优先搜索就是我们二叉树的中序遍历。

先找到一条路,一搜到底,走到头了我们再回来,继续搜索。

DFS总结

image-20220326195305608

递归就是深度优先搜索(DFS)。

BFS — 广度优先搜索

以层序遍历的方式进行遍历,把树分层,按层搜索,先搜索第一层,再搜索第二层…

image-20220326195728009

我们从起点开始搜,从第一层搜到第二层:

image-20220326200443965

由第二层的节点扩展搜索到第三层:

image-20220326200634694

那么广搜我们怎么实现呢?我们知道,深搜用的是递归,借助的是系统栈,广搜其实是借助我们的队列。

先将第一层的入队,然后找到第二层的所有节点,将第一层的节点出队,随后将第二层的节点全部入队,以此类推。

我们可以发现,这个节点在第几层,就代表着我走几步能走到这个节点。

但我们这样搜索,很大的概率会有重复的值,那我们可以做一个这样的假设,我们要寻找到 ( 3 , 3 ) (3,3) (3,3)这个点:

image-20220326203009151

我们走左子树需要3步,走右子树需要4步,当我们要求到点 ( 3 , 3 ) (3,3) (3,3)的最短路径时,我们广度优先搜索的优势就体现出来了,当我们第一次遍历到结果时,那答案必然就是最短路径。

所以我们经常使用广度优先搜索求最优解的问题。

BFS总结

image-20220326203722366

搜索总结

image-20220326203811819

区分DFS与BFS

image-20220326205448823

此时大家可能还会觉得有点抽象,我们举一个具体的例子利用代码来分析:

image-20220326205640644

DFS,关注圈红代码区域,是代码的不同点。

image-20220326210308029

BFS,每一个颜色都是一层。

image-20220326211602596

剪枝

其实我们的代码中就有剪枝:

image-20220326211901931

一些更细节的剪枝:

image-20220326211927975

具体的例子剪枝:

image-20220326211949034

对于具体问题的特判,就是剪枝。

以上都是显示图的搜索,那接下来我们看看隐式图的搜索:

image-20220326212423578

一看是最小步数,那我们肯定优先想到bfs。

广搜就是从根节点开始往下找,找到一个最近的、符合条件的一个点。

BFS、双向BFS、A*启发式搜索

image-20220501113744449

双向BFS也比较少见,但它会使我们搜索的面积变小,对于某些题还是非常合适的。

image-20220501114011172

image-20220501114359654

这里只是简单介绍一下A*搜索,一般情况下是遇不到的,只有在项目里有特殊需求,或者ACM竞赛才会遇到。

估价函数:把所有的下一个可能进行排序,选一个权值最小的点作为下一步,它没有走多余的步骤。

image-20220501114916892

经过上图 想必大家的理解会更清晰了。

LeetCode真题

DFS

LeetCode130. 被围绕的区域

难度:mid

在一个矩阵中,把所有被'X'围起来的'O'变为'X',那么最外层的'O'一定不会变为'X',同时与最外层的'O'相邻的也不会变为'X'

我们可以把这些'O'标记成别的符号,例如'#',再遍历一遍矩阵把剩下的'O'变为'X',最后把'#'还原为'O'即可。

LeetCode题解:代码实现


LeetCode494. 目标和

难度:mid

对于三个数 A B C ABC ABC,我们可以得到这样的一棵树:

image-20220501230401643

我们可以dfs遍历所有情况,用 t a r g e t + n u m s [ i ] target + nums[i] target+nums[i] 或者 t a r g e t − n u m s [ i ] target - nums[i] targetnums[i] 来判断拼凑出来的每个方案的值是否为 0 0 0,如果为 0 0 0 则说明找到了一种答案,把所有方案数相加即可。

LeetCode题解:代码实现


LeetCode473. 火柴拼正方形

难度:mid

给我们若干个火柴的长度,把它们拼成一个正方形,且每个火柴都必须使用一次。

如果这些火柴能拼成正方形,那它们的总长度 s u m sum sum 肯定可以被 4 4 4 整除。

把所有火柴分成 4 4 4 份,且每份的长度都相等 ( s u m / 4 ) (sum/4) (sum/4),我们可以用dfs遍历所有的情况。

这道题必须剪枝,不剪枝会超时,参考代码注释。

LeetCode题解:代码实现


LeetCode39. 组合总和

难度:mid

在一个数组中选任意个数凑成 t a r g e t target target 目标值,数组中的每个数可以选任意次

这道题比较简单,直接看代码。

LeetCode题解:代码实现


LeetCode51. N 皇后

难度:hard

经典的N皇后问题,一个皇后的横、竖以及斜方向都不能有别的皇后。

这道题其实可以用全排列的思想做,我们枚举每一行的每个位置是否能放,如果能放则标记它的竖 c o l col col 、正对角线 d g dg dg 以及反对角线 u d g udg udg t r u e true true

另一种做法就是比较原始的搜索,上面的方法其实我们已经把问题抽象了,那么这个方法就是搜索每一个格子,是否能放皇后,当枚举到最后一个格子时,搜索也完成了。

这里更推荐第一种方法。

LeetCode题解:两种方法代码实现


BFS

LeetCode993. 二叉树的堂兄弟节点

难度:easy

DFS

我们先采用比较熟悉的dfs,从根节点开始遍历,当遍历到 x x x 时,我们记录一下它的深度以及父节点的值,遍历到 y y y 时同理,如果深度相同且父节点不同,则返回 t r u e true true

BFS

需要借助队列,先存入根节点,从根节点开始一层一层的搜索,搜索到这个节点就判断当前节点的值是否是我们的目标值,将每个节点的左右节点都 p u s h ( ) push() push() 进去, d e e p t h + 1 deepth + 1 deepth+1,判断完之后 p o p ( ) pop() pop() 掉。

我们可以做一个剪枝,当我们已经遍历到 x x x y y y,我们可以直接 b r e a k break break,不需要遍历剩下的节点。

LeetCode题解:两种方法代码实现


LeetCode542. 01 矩阵

难度:mid

每一个 1 1 1 找到离它最近的一个 0 0 0 ,返回这个新的数组。

我们看示例2:

image-20220501143752137

我们可以用bfs逆向搜索,找到对于某个 0 0 0 离这个 1 1 1 最近的距离,并标记在数组中。

每一个 1 1 1 都是被它最近的 0 0 0 扩散到的。

LeetCode题解:两种方法代码实现


LeetCode1091. 二进制矩阵中的最短路径

难度:mid

这道题就可以看做是BFS的经典例题了,直接看代码吧。

LeetCode题解:代码实现


LeetCode752. 打开转盘锁

难度:mid

给了我们一个转盘锁的数组 d e a d e n d s deadends deadends,记录着 “死亡数字”,从起始的 0000 0000 0000 状态转到我们的目标 t a r g e t target target 状态,期间不能与 d e a d e n d s deadends deadends 数组元素(死亡数字)相同;能解锁则返回最小步数,否则返回 − 1 -1 1,此题仍然用bfs求解。

第一个样例就很清晰明了,注意中的字样转到了死亡数字 0102 0102 0102,故这种方式无法解锁:

image-20220501184421997

我们可以把死亡数字理解成迷宫中的墙,标记一下。

LeetCode题解:代码实现


LeetCode剑指 Offer 13. 机器人的运动范围

难度:mid

也是很基础的bfs,只不过条件是下标的数位之和不能大于 k k k

LeetCode题解:代码实现


总结

举报

相关推荐

0 条评论