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

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

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

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


算法理解

刚刚接触到深度优先搜索(depth-first searches,DFS),先以一个实际案例简单谈谈自己的看法。
最先使用深度优先算法,是在迷宫问题上,需要在给定起点和终点的一个10*10大小的迷宫(以0-1值矩阵等效)中寻找可能的路径。
深度优先算法对于该问题,核心是以栈为基础,利用先进先出的机制,在遇到“死胡同”之后,一步步退回到上一个有其他路径选择的位置,选择其他的路径进行下一步的迭代。
由此认识到了深度优先搜索,虽然不一定能够找到最优路径,但也是迷宫问题的一种非常接近实际情况的解法思路:可以模拟为一个人在迷宫中在每一个岔路口对每一种走法进行尝试,直到某一条路不通(则放弃这种走法)或者某一条路到达终点,DFS将这种思路以算法的方式进行实现。
与广度优先搜索的区别,核心在于:广度优先搜索是基于队列(先进先出)的机制,在每一个分叉路口,考虑每一种的可能性,从而产生各个分支加入到队列中,后续同步对当前队列中所有的可能性进行搜索,能够保证搜索到其中一条的最优路径。


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

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

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

1.题干

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

2.思路

利用深度优先搜索方法的话,最便捷的办法是利用递归的方法,从根节点开始自每一个节点对该节点的每一种分支进行递归,直到各个叶子节点,返回该叶子节点对应的路径。

3.代码

class Solution:
    def binaryTreePaths(self, root: TreeNode) -> List[str]:
		paths = []
        def searchway(root,path):
            if root:	#迭代函数中的if和while如何选择,老毛病了,要注意
                path += str(root.val)	#对于每一次迭代来说,先将非空节点的值保存到path变量中
                if not root.left and not root.right:	#判断是否为叶子节点
                    paths.append(path)	#如果是叶子节点,将该节点对应的路径存储到paths结果列表中
                else:	#如果不是叶子节点,说明还有左子节点,或右子节点
                    path += "->"	#将结果中要求的字符串格式添加进变量
                    searchway(root.left,path)	#对左右子节点进行迭代,注意变量的存储,递归函数要多注意函数中变量如何定义,从而实现函数的迭代作用
                    searchway(root.right,path)
                    
        searchway(root,"")	#从根节点开始迭代
        return paths    

4.总结

1.算法实现时的思维灵活性
在解决实际问题的过程中,深度优先算法更多的是一种思想,应该植入到我们在寻求问题解决路径的思维方法库中,要避免生搬硬套。
我刚开始思考这个题时,总是收到迷宫问题的影响,想要硬搬栈来套用,但是遇到了一个在这里不适用的一个问题:这里树节点是类似链表的节点类型,不能向前位来寻址回退,所以不适合套用栈,以出栈的方式来回到前一个分支点。
看了别人的解法之后,发现没必要一定要用到那种数据结构,重要的是用对解决问题最便利的思想,最简单的算法实现手段来进行编码,这里就直接用迭代的实现方法,结合深度搜索思想,非常便捷的解决了问题。
切忌生搬硬套,从实际问题本身出发!!!

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

二、200题:岛屿数量

1.题干

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

示例

2.思路

要找岛屿的数量,也就是在0-1矩阵中寻找被0隔开的1的“块数”,看着简单,最后还是看评论区题解才想到的。。。

最直白的方法就是对矩阵进行遍历,遇到1时,即碰到了岛屿,把这个岛屿铲平,然后继续出发,直到遇到下一个岛屿,继续铲平,循环往复。。。(有点海盗的感觉昂,要成为海贼王的男人!!!)

寻找岛屿很简单,遍历矩阵就可以了,遇到1就是找到岛屿了,问题是怎么铲平这个岛屿(滑稽),这里采用深度优先搜索方法,碰到1之后,对它上下左右四个方向上进行检查,如果为1(是陆地!!!),就把它变为0(铲平!),并继续向四周查找陆地,循环迭代。这里的迭代情况比较明显,过程也很简单,就是两步:

  1. 把1变成0,铲平这个陆地;
  2. 向四周查找1,以本地为基础继续探查陆地;

3.代码

class Solution:
    def dfs(self, grid, row, col):	#深度优先搜索,通过递归的方式实现,根据碰到的陆地(1),递归寻找周边的陆地,并铲平所有陆地
        grid[row][col] = 0	#铲平这个陆地
        row_max = len(grid)	#确定搜素范围
        col_max = len(grid[0])

        for x, y in [(row-1,col), (row+1,col), (row,col-1), (row,col+1)]:	#确定每次搜索陆地的方向
            if 0 <= x < row_max and 0 <= y < col_max and grid[x][y] == "1":	#这里要注意,我开始使用的是x in range(row_max)的形式,自以为灵活,其实这样速度要慢不少,还是直接的比较会快很多
                self.dfs(grid, x, y)	#递归迭代
    
    def numIslands(self, grid: List[List[str]]) -> int:
        row_max = len(grid)
        res = 0
        if row_max == 0:
            return res
        
        col_max = len(grid[0])
        for x in range(row_max):	#遍历寻找岛屿
            for y in range(col_max):
                if grid[x][y] == "1":
                    res += 1	#如果碰到陆地,就把岛屿计数值加一
                    self.dfs(grid, x, y)	#铲平碰到的陆地所在的岛屿
        return res

4.总结

这题是利用深度优先搜索来寻找一个陆地所在的整个岛屿的范围,与迷宫问题在本质上的相似度还是很高的,理解起来不困难。

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

1. 题干

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

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

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

2. 解题思路

二分法 + 深度优先搜索
1)将问题转化为如果只走消耗值小于某个值的路径的话,能不能达到终点的问题,实测采用深度优先搜索要比广度优先搜索更快;
2)利用二分法,从0~10e6-1范围开始二分,如果二分后能够到达,则缩小区间,如果二分后不能到达,则放大区间;

3. 代码

class Solution:
    def minimumEffortPath(self, heights: List[List[int]]) -> int:
        m = len(heights)
        n = len(heights[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)
        ]
		#二分法结合深度优先搜索
        left, right = 0, 10e6 - 1
        while left < right:
            mark = [[0] * n for _ in range(m)]
            stack = [[0,0]]
            mark[0][0] = 1
            mid = left + (right - left) // 2
            while stack:
                cur_p = stack[-1]
                if cur_p == [m-1, n-1]:
                    break
                for direct in directs:
                    next_p = direct(cur_p[0], cur_p[1])
                    if 0 <= next_p[0] < m and 0 <= next_p[1] < n and mark[next_p[0]][next_p[1]] == 0 and abs(heights[next_p[0]][next_p[1]] - heights[cur_p[0]][cur_p[1]]) <= mid:
                        stack.append([next_p[0], next_p[1]])
                        mark[next_p[0]][next_p[1]] = 1
                        break
                else:
                    stack.pop()
            if mark[-1][-1] == 1:
                right = mid
            else:
                left = mid + 1
        return int(left)

四、417题:太平洋大西洋水流问题

1. 题干

给定一个 m x n 的非负整数矩阵来表示一片大陆上各个单元格的高度。“太平洋”处于大陆的左边界和上边界,而“大西洋”处于大陆的右边界和下边界。
规定水流只能按照上、下、左、右四个方向流动,且只能从高到低或者在同等高度上流动。
请找出那些水流既可以流动到“太平洋”,又能流动到“大西洋”的陆地单元的坐标。

提示:
输出坐标的顺序不重要
m 和 n 都小于150

2. 解题思路

一开始最直接想到的是,对矩阵中的每个点进行两次搜索,看看能否到达两个海洋,但是这样处理的时间复杂度会非常大,看了题解之后,此题使用逆向思维:
1)从两个海洋出发(海洋对应矩阵的边),查找矩阵中能够流向该海洋的点
2)深度优先搜索的迭代写法,分别从矩阵的各个边出发,得到能够流到两个海洋的点的集合,然后再对两个结果取交集,即可得到结果:

3. 代码

class Solution:
    def pacificAtlantic(self, matrix: List[List[int]]) -> List[List[int]]:
        m = len(matrix)
        if m == 0:
            return []
        n = len(matrix[0])
        enable1 = set()
        enable2 = set()

        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)
        ]
		#深度优先搜索的迭代写法,代码简单,时间、空间复杂度都较低,但是要注意细节处理
        def dfs(matrix, x, y, res):
        #迭代写法,需要将搜索得到的点的集合,作为参数传入,每次判断新的点是否符合条件,符合的加入集合
            res.add((x,y))
			#对新的点的四个方向进行判断,是否符合条件
            for direct in directs:
                next_p = direct(x, y)
                if 0 <= next_p[0] < m and 0 <= next_p[1] < n and matrix[next_p[0]][next_p[1]] >= matrix[x][y] and next_p not in res:
                #如果某个方向的点符合条件,就将这个点作为新的点,进行递归迭代
                #这样最终没有点满足条件之后,就能够得到所有能够流到目标海洋中的点的集合
                    dfs(matrix, next_p[0], next_p[1], res)
        #对四条边分别进行处理,分别得到能够流到两个海洋中的点的坐标集合,取交集得到最终结果
        for i in range(m):
            dfs(matrix, i, 0, enable1)

        for j in range(n):
            dfs(matrix, 0, j, enable1)

        for i in range(m):
            dfs(matrix, i, n-1, enable2)

        for j in range(n):
            dfs(matrix, m-1, j, enable2)

        return list(enable2 & enable1)

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值