leetcode刷题笔记——广度优先搜索

leetcode刷题笔记——广度优先搜索

目前完成的广度优先搜索相关的leetcode算法题序号:
简单:257
中等:200, 1631

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/reconstruct-itinerary
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。

文章目录

    • leetcode刷题笔记——广度优先搜索
  • 算法理解
  • 一、257题:二叉树的所有路径
    • 1.题干
    • 2.思路
    • 3.代码
    • 4.总结
  • 二、200题:岛屿数量
    • 1.题干
    • 2.思路
    • 3.代码
    • 4.总结
  • 三、1631题:最小体力消耗路径
    • 1. 题干
    • 2. 解题思路
    • 3. 代码


算法理解

广度优先搜索在解题时,是针对搜索类问题与深度优先搜索相对的两种解题思路。
究竟是每次针对一种情况先一条道走到黑,如果不行换另一条路(深度优先),还是在每一步上都考虑所有的可能性,同步的去寻找所有的可能结果(广度优先)。针对不同的问题,这两种思路都有自己的优越性,具体如何选择自己慢慢体会吧。
初次遇到广度优先搜索,是在迷宫问题上,和深度优先搜索作为迷宫问题的两种解法思路碰到的。迷宫问题可以简单描述为:针对一个0-1矩阵表示的迷宫,给定起点和终点,寻找一条由起点到终点的路径。

正如深度优先搜索刷题笔记中提到的,这个题用深度优先搜索实现起来更直观简单一些。广度优先搜索虽然在理论上理解起来也没有太大的障碍,但是在实现的过程中,使用队列的时候,对于算法新手的我来说,由于不是很熟悉队列数据结构的使用,对于进队出队,出队后元素如何存储在路径记录表中的顺序关系不太熟悉,而且到达终点之后如何在路径记录表中按照记录的索引值找到对应的完整路径也造成一定的麻烦,所以在运用广度优先搜索算法的时候,确实会有一定的麻烦。

不过广度优先搜索算法在寻找路径的过程中,不需要在遇到死胡同之后频繁的进行“回溯”操作,所以在时间复杂度上一般要低一些(对应的空间复杂度一般要比广度优先搜索要高一点),而且广度优先搜索算法能够保证找到的一定是最短路径中的一种情况。


提示:以下是深度优先搜索的刷题笔记

一、257题:二叉树的所有路径

二叉树是指每个节点至多有2个分支的树结构,包括满二叉树和完全二叉树等特殊结构,本题的对象是普通二叉树。

1.题干

给定一个二叉树,返回所有从根节点到叶子节点的路径。
给定的树结构是带有左右子节点的数据节点,原始数据为根节点。

2.思路

广度优先搜索基于队列(先进先出)的机制,利用广度优先搜索算法进行解题时,基本的思路就是在每一步考问题的所有可能性,将所有可能性添加到队列中,然后按照队列的顺序依次对所有可能性同步逐个进行处理。
在这个题目中,初始节点即为根节点,从根节点开始迭代,每一步考虑的可能性就是是否有左右子节点,如果有左右子节点,就更新各种可能性下的路径,并将各种可能性更新到队列中,如果达到终止条件:搜索到达叶子节点(无左右子节点),则将路径输出。

3.代码

class Solution:
    def binaryTreePaths(self, root: TreeNode) -> List[str]:
        paths = []
        if not root:#如果根节点为空,则没有节点,也就不存在路径
            return paths

        node_queue = collections.deque([root])#建立节点的队列,用于存储二叉树分支搜索每一步的可能性
        path_queue = collections.deque([str(root.val)])#路径队列,用于存储与节点队列对应的各种路径,记录顺序必须一致
        while len(node_queue) > 0:#节点队列不为空,就一直循环
            node = node_queue.popleft()#导出节点队列中最前面的一个节点,按顺序逐一处理
            path = path_queue.popleft()#导出与节点对应的路径

            if not node.left and not node.right:#如果队列最前方的节点是叶子节点,则将路径存储到结果列表中,这个叶子节点已经出队,不会在后续重复考虑
                paths.append(path)
            if node.left:#如果当前节点有左右子节点,则将左右子节点添加到节点队列中,并更新各个子节点对应的路径,加入到路径对应中,保证了节点队列与路径队列的顺序一一对应
                node_queue.append(node.left)
                path_queue.append(path + '->' + str(node.left.val))
            if node.right:
                node_queue.append(node.right)
                path_queue.append(path + '->' + str(node.right.val))             
        return paths

4.总结

1.广度优先搜索关键是要抓住如何队列的机制,按顺序保存并逐个同步处理所有可能性
由于是每次迭代要考虑所有的可能性,并将每一步所有的可能性全部按顺序存储在队列中,所有在每次迭代要考虑的东西就会比较多,在进行循环的时候,实现起来也会更抽象一些,不像深度优先那样,不管三七二十一,我先一条道走到黑,不行了就走回去试试其他的可能性。
所以关键还是要熟练掌握广度优先搜索这种思想,只要是在每一步迭代中,依次考虑各种可能性,再将各个可能性下与结果相关的内容按顺序进行存储,再循环逐个进行处理,就能够实现广度优先搜索了。
剩下的就是如何将各种实际问题提炼到广度优先搜索的这一步了,这没办法,只能多练习,对广度优先搜索足够熟悉,在碰到相关的问题的时候自然就能灵光一闪啦。

2.与深度优先的比较
这个题当然还可以使用广度优先算法进行解决,相比于深度优先算法的迭代写法,代码实现要麻烦一些。在迷宫问题中,我一直觉得是不是广度优先要比深度优先在各方面都优秀一些,但是从这一题可以发现,深度优先在代码实现上,结合递归之后,实现上更简单,代码理解起来也更容易一点。
查资料找到的两者对比:
1.深度优先搜索有递归和非递归两种实现方法,对于深度较小,递归效果明显时使用递归实现会更简单易懂;
2.深度优先搜索如果每一次向下的搜索不是保留各深度的全部节点,只保存当前迭代对应的各个节点(比如迷宫问题),则需要存储的节点数,近似为节点的深度值,占用的空间比较少,因此,在搜索深度很大的场合,使用深度优先搜索能够有效避免内存溢出的情况;(深度优先占内存小,广度优先占内存大)
3.深度优先搜索常用栈(先进后出)结构,广度优先搜索常用队列(先进先出)结构;
4.深度优先搜索有进栈、出栈操作,处理速度要低于广度优先搜索;

二、200题:岛屿数量

1.题干

给你一个由 ‘1’(陆地)和 ‘0’(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。

leetcode刷题笔记——广度优先搜索_第1张图片

2.思路

这一题在深度优先搜索算法那一篇已经讲过,在这里提一下广度优先搜索的解法。
题目的要求是找到有几个岛屿,解题的总体思路不变,依然是对矩阵进行检索,寻找陆地,找到陆地之后搜索与该陆地相连的所有陆地。深度优先跟广度优先的区别也只是体现在寻找某一块陆地相连的其他陆地的方法上,而且实现中的差别,在这一题中其实也不大,直接看代码吧。

3.代码

class Solution:
    def numIslands(self, grid: List[List[str]]) -> int:
        row_max = len(grid)#确定搜索边界
        col_max = len(grid[0])
        res = 0

        if row_max == 0:#如果矩阵为空,则没有岛屿
            return res
        
        q = collections.deque()#用于存储每个陆地坐标的队列,每一次遇到陆地,对它前后左右的地块进行检查,如果是陆地就加进队列,将其铲平的同时,也对它的周围进行搜查
        for x in range(row_max):
            for y in range(col_max):#对矩阵依次进行检索
                if grid[x][y] == "1":#如果遇到陆地,将其铲平,并加入陆地坐标存储队列
                    grid[x][y] = "0"
                    res += 1
                    q.append((x,y))
                    while q:#对陆地地块的四周开始进行广度优先搜索
                        row, col = q.popleft()#导出陆地地块的坐标
                        for (r, c) in [(row-1,col), (row+1,col), (row,col-1), (row,col+1)]:#查看坐标四周是否有陆地
                            if 0 <= r < row_max and 0 <= c < col_max and grid[r][c] == "1":#如果有陆地将其铲平,并将其坐标存储到队列中,注意搜索的坐标范围,超出范围的坐标不予考虑
                                grid[r][c] = "0"
                                q.append((r,c))
        return res

4.总结

把握题意,理解了广度优先搜索的作用之后,实现起来细心一点就没问题啦

三、1631题:最小体力消耗路径

1. 题干

你准备参加一场远足活动。给你一个二维 rows x columns 的地图 heights ,其中 heights[row][col] 表示格子 (row, col) 的高度。一开始你在最左上角的格子 (0, 0) ,且你希望去最右下角的格子 (rows-1, columns-1) (注意下标从 0 开始编号)。你每次可以往 上,下,左,右 四个方向之一移动,你想要找到耗费 体力 最小的一条路径。

一条路径耗费的 体力值 是路径上相邻格子之间 高度差绝对值 的 最大值 决定的。

请你返回从左上角走到右下角的最小 体力消耗值 。

2. 解题思路

第一反应就是广度优先搜索,此题用广度优先搜索也没有问题,有一个关键的问题就是如何在每一步确定各种路径是否有必要继续走下去
这里在这个问题上,采用的DP思想,新建一个与heights相同形状的dp标记矩阵,每种走法走到某个位置的时候,将该路径当前的最大体力消耗值与标记矩阵在该位置的值进行比较,如果小于则更新标记矩阵,否则放弃这种走法(因为达到这个位置之后,后续的最优路径与前面的路径无关了,如果前面路径到达这个位置的消耗没有更小,就直接放弃这种走法)

3. 代码

class Solution:
    def minimumEffortPath(self, heights: List[List[int]]) -> int:
        path_queue = collections.deque([(0,0)])
        #定义dp标记矩阵,初始化每个元素值为正无穷
        dp = [[math.inf] * len(heights[0]) for _ in range(len(heights))]
        dp[0][0] = 0
        #每个位置的移动方向定义
        directs = [
                lambda x, y : (x + 1, y),
                lambda x, y : (x, y + 1),
                lambda x, y : (x - 1, y),
                lambda x, y : (x, y - 1)]
        
        while len(path_queue) > 0:
            cur_path = path_queue.popleft()
			#在每个位置对每种走法进行遍历
            for direct in directs:
                next_path = direct(cur_path[0], cur_path[1])
                #如果这个方向没有超出边界
                if 0 <= next_path[0] < len(heights) and 0 <= next_path[1] < len(heights[0]):
                #求解下一步走到的位置的消耗,并得到这种走法下的最大消耗
                    next_cost = abs(heights[next_path[0]][next_path[1]] - heights[cur_path[0]][cur_path[1]])
                    next_cost = max(next_cost, dp[cur_path[0]][cur_path[1]])
                    #如果这种走法下的消耗小于dp标记矩阵在该处的值(前面的走法达到该位置的最大消耗)
                    if next_cost < dp[next_path[0]][next_path[1]]:
                    #更新dp标记矩阵在该位置处的值,并将该走法继续下去(将其加入队列)
                        dp[next_path[0]][next_path[1]] = next_cost
                        path_queue.append(next_path)
        #循环结束后,返回dp标记矩阵的右下角位置的元素值
        return dp[len(heights)-1][len(heights[0])-1]

这种路径搜索的问题,需要注意如何确定每种路径的终止条件

你可能感兴趣的:(数据结构与算法,队列,python,深度搜索)