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变成0,铲平这个陆地;
- 向四周查找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)