DFS(深度优先搜索)做题笔记
代码用Python
题来自Acwing和LeetCode
有不对的地方欢迎评论私信指正
会持续更新,欢迎收藏追更
如果不了解DFS可以去B站找找教程,该文章默认会DFS
经典DFS问题
842. 排列数字
每层枚举一个未使用过的数,填到答案中,要注意每个数的状态,防止不能枚举或重复枚举。
Code
n = int(input())
ans = [0 for _ in range(n)] # 存放答案
t = [False for _ in range(n+1)] # 记录每个数的使用情况
def dfs(u):
# u表示深度
# 如果当前深度等于n,则说明已经获取到一种排列,此时输出ans中的数,停止递归
if u == n:
for i in range(n):
print(ans[i], end=' ')
print()
return
else:
# 反之则说明还有空没填上
# 此时枚举一下1 - n中的数,如果当前的数没被使用过,则添加到ans中,并标记为使用过
# 再递归下一层
for i in range(1, n+1):
if not t[i]:
t[i] = True
ans[u] = i
dfs(u + 1)
# 当递归出来时,将当前使用的数恢复为未使用,即状态还原,使得下一次还能够使用该数
t[i] = False
dfs(0)
843. n-皇后问题
很经典的DFS问题了,DFS递归每行,再在DFS中枚举每列,判断该点对应的列、对角线和反对角线是否被使用过,如果没有,则在该点填上一个皇后,再将该列、对角线和反对角线标记为使用过,再继续递归下一行,记得状态恢复。
Code
n = int(input())
g = [['.' for _ in range(n)] for _ in range(n)] # 存放棋盘
col = [False for _ in range(n)] # 记录列的使用情况
dg = [False for _ in range(n * n)] # 记录斜线的使用情况
udg = [False for _ in range(n * n)] # 记录反斜线的使用情况
def dfs(u):
# 在递归完棋盘时,打印棋盘
if u == n:
for i in range(n):
for j in range(n):
print(g[i][j], end='')
print()
print()
return
else:
# 反之枚举每一列
for i in range(n):
# 当前列未被使用,当前点对应的斜线反斜线未被使用
if not col[i] and not dg[u - i] and not udg[u + i]:
# 标记为使用
col[i] = dg[u - i] = udg[u + i] = True
# 在该点放一个皇后
g[u][i] = 'Q'
# 继续递归下一行
dfs(u + 1)
# 状态恢复
g[u][i] = '.'
col[i] = dg[u - i] = udg[u + i] = False
dfs(0)
下面是一些DFS对树的操作
100.相同的树
递归的当前问题:当前节点是否相同
两种情况 1.同时为空也算相同,return True
2.只有一个为空或者值不相同,则return False
递归的子问题:当前节点的左右节点是否和另外一个节点的左右子树相同。
Code
class Solution:
def isSameTree(self, p: Optional[TreeNode], q: Optional[TreeNode]) -> bool:
def dfs(node1, node2):
# 当前问题
# 如果两节点同时为空,则说明遍历到底了也是相同的,return True
if not node1 and not node2:
return True
# 其中有一个为空
if (not node1) ^ (not node2):
return False
# 两节点都不为空,但val值不相同
if node1.val != node2.val:
return False
# 子问题
# 继续递归左右子树,判断是否相同,用and连接
return dfs(node1.left, node2.left) and dfs(node1.right, node2.right)
return dfs(p, q)
104.二叉树的最大深度
DFS向下递归,参数中包含一个count,递归到每层都+1,如果出现比res要大的count,则更新res。
递归当前问题:当前层是否为最大深度
子问题:左右子树代表的层是否为最大深度
Code
class Solution:
def maxDepth(self, root: Optional[TreeNode]) -> int:
res = 0 # 记录最大深度
def dfs(node, count):
# 递归停止条件
if not node:
return 0
# 递归到新的一层
count += 1
# 在当前节点为叶子节点时,判断一下当前层的深度和最大深度的谁大,更新res
if not node.left and not node.right:
nonlocal res
if count > res:
res = count
# 继续递归下一层
dfs(node.left, count)
dfs(node.right, count)
dfs(root, 0)
return res
111.二叉数的最小深度
递归当前问题:当前层的该节点是否为叶子节点,如果是,则当前叶子节点所代表的深度是否为最小深度
子问题:下一层是否为最小深度
Code
class Solution:
def minDepth(self, root: Optional[TreeNode]) -> int:
if not root:
return 0
# 不能取0,没有比0更小的深度了,res更新不了
# 也可以在递归里判断一下是否为第一次递归,如果是则不用对比直接更新res
res = inf
def dfs(node, count):
# 递归停止条件
if not node:
return 0
# 到当前层深度加一
count += 1
# 当前节点为叶子节点时,则判断一下当前层的深度是否比res小,更新res
if not node.left and not node.right:
nonlocal res
if count < res:
res = count
# 继续递归下一层
dfs(node.left, count)
dfs(node.right, count)
dfs(root, 0)
return res
112.路径总和和113.路径总和II和上面两题类型相同,可以做一下巩固,还有129.求根到叶子节点数字之和,就懒得写那么多相同的了,有兴趣就做做。
112.路径总和
Code
class Solution:
def hasPathSum(self, root: Optional[TreeNode], targetSum: int) -> bool:
def dfs(node, Sum):
# 找到底了都没找到一个合适的路径,则return False
if not node:
return False
# 更新当前路径的和
Sum += node.val
# 如果当前节点为叶子节点,则判断一下当前路径和与目标路径和是否相同
if not node.left and not node.right:
if Sum == targetSum:
return True
# 继续递归下一层
return dfs(node.left, Sum) or dfs(node.right, Sum)
return dfs(root, 0)
113.路径总和II
Code
class Solution:
def pathSum(self, root: Optional[TreeNode], targetSum: int) -> List[List[int]]:
ans = []
path = [] # 存放路径中的节点
def dfs(node, Sum):
if not node:
return
# 增添当前路径中的节点
path.append(node.val)
# 更新当前结果Sum
Sum += node.val
# 如果当前节点为叶子节点
if not node.left and not node.right:
# 当前路径和满足目标和
if Sum == targetSum:
# 将当前路径添加到答案中
# 如果直接使用ans.append(path),则在后续path改变时,在ans内的path也会随之改变,因为地址相同
ans.append(path.copy())
# 正确做法是,单独copy一份出来,创建新的地址存放
# 遍历左右子树
dfs(node.left, Sum)
dfs(node.right, Sum)
# 要注意状态恢复
# 相当于利用栈去遍历二叉树那样,当遍历到底时,弹出,继续向上递归,直到当前节点存在右子树,则继续从右子树重新追加
# 如果不恢复,则会导致路径叠加
path.pop()
dfs(root, 0)
return ans
199.二叉树的右视图
利用一个列表和控制递归顺序,使得列表中能存储每层最左边的节点。
Code
class Solution:
def rightSideView(self, root: Optional[TreeNode]) -> List[int]:
ans = [] # ans中存放的是当前深度(depth)下遇到的第一个元素
def dfs(node, depth):
if not node:
return
# 如果当前的depth等于ans的长度,则说明当前遍历到的元素是当前深度下遇到的第一个元素
if depth == len(ans):
ans.append(node.val)
depth += 1 # 更新深度,去下一层
# 通过控制递归顺序,因为要取最右侧的元素,所以先递归右子树保证每层最先访问的是右节点
dfs(node.right, depth)
dfs(node.left, depth)
dfs(root, 0)
return ans
117.填充每个节点的下一个右侧节点指针 II
也是利用一个列表和控制递归顺序,使得列表中能存储每层最左边的节点,再利用每层最左边的节点指向相同层的其他节点,再将被指向的节点更新为当前层最左边的节点,重复。
Code
class Solution:
def connect(self, root: 'Optional[Node]') -> 'Optional[Node]':
pre = {} # pre存储的是每层最左侧的节点
def dfs(node, depth):
if not node:
return
# 当前深度depth遇到的第一个节点
if depth == len(pre):
pre[depth] = node
# 如果为深度depth的其他节点,则让最左侧节点的next指针指向当前节点
# 再将最左侧节点更新为当前节点
else:
pre[depth].next = node
pre[depth] = node
# 递归下一层
depth += 1
dfs(node.left, depth)
dfs(node.right, depth)
dfs(root, 0)
return root
下面是DFS对一些岛屿问题的解决
695.岛屿的最大面积
通过DFS对每个土地的四周进行搜索,如果搜索到的是土地,则面积加一,并将该土地设置为访问过,继续向当前土地的四周继续搜索;反之如果为海洋,则不加一,并停止搜索,同时也要注意当前搜索范围是否在矩阵中。最后获得所有岛屿面积后取max。
Code
class Solution:
def maxAreaOfIsland(self, grid: List[List[int]]) -> int:
def dfs(i, j, count):
# 越界
if i < 0 or i >= len(grid) or j < 0 or j >= len(grid[i]):
return 0
# 遇到了海洋
if grid[i][j] == 0:
return 0
# 初始化,表示当前遍历到的这块土地
count = 1
# 将遍历过的土地用1标记为使用过
grid[i][j] = 0
# 向当前土地的四周找到完整的一块岛屿
# 因为在每次递归中,只要是土地,count就为1,所以要将所有递归中的count累加,最后返回到递归入口的就是岛屿的面积
count += dfs(i+1, j, count) + dfs(i-1, j, count) + dfs(i, j+1, count) + dfs(i, j-1, count)
return count
ans = 0
for i in range(len(grid)):
for j in range(len(grid[i])):
# 以遇到土地为递归入口,因为在dfs中遇到土地使用后用0标记,所以不会重复调用同一块土地
# 求max
if grid[i][j] == 1:
ans = max(ans, dfs(i, j, 0))
return ans
463.岛屿的周长
从土地到土地,周长不变,从土地到海洋,周长加一,所以对一个土地向四周搜索,如果当前方向的下一个格子为海洋,则返回1,并停止搜索;如果当前方向的下一个格子为土地,则周长不变,将当前土地标记为使用过,防止重复访问,继续向四周搜索。
如果当前格子为土地且向某一方向搜索时会越界,则也周长加一。
Code
class Solution:
def islandPerimeter(self, grid: List[List[int]]) -> int:
def dfs(i, j):
# 遇到边界,则说明岛屿蔓延到了边界上,超出边界就加一
if i < 0 or i >= len(grid) or j < 0 or j >= len(grid[0]):
return 1
# 如果遇到了海洋,则说明从土地到了海洋,周长加一
if grid[i][j] == 0:
return 1
# 如果遇到已经遍历过的土地,则直接停止,不增加周长
if grid[i][j] == 2:
return 0
grid[i][j] = 2 # 遇到的土地标记为2,防止重复递归
# 向土地的四周蔓延,找出整个岛屿
return dfs(i+1, j) + dfs(i-1, j) + dfs(i, j+1) + dfs(i, j-1)
ans = 0
for i in range(len(grid)):
for j in range(len(grid[i])):
if grid[i][j] == 1:
ans += dfs(i, j) # 递归入口,从遇到的第一个土地开始
return ans
200.岛屿数量
对相连通的1都修改为0,在修改完后遍历过程中又出现了1,则说明遇到了另一个岛屿,重复,重复次数即为结果。
Code
class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
# 通过dfs找出整个岛屿,并用0标记
def dfs(i, j):
if i < 0 or i >= len(grid) or j < 0 or j >= len(grid[i]) or grid[i][j] == "0":
return
grid[i][j] = "0"
dfs(i+1, j)
dfs(i-1, j)
dfs(i, j+1)
dfs(i, j-1)
return
count = 0
for i in range(len(grid)):
for j in range(len(grid[i])):
# 遇到了几次1,就有几个岛屿
# 因为dfs会将相连通的土地用0覆盖,即整个岛屿用0覆盖,再遇到了1则说明有其他岛屿
if grid[i][j] == "1":
dfs(i, j)
count += 1
return count
130.被围绕的区域
被围绕的定义是不与边界上的’O’直接相连。所以我们只要对边界上的’O’和与之接壤的’O’进行搜索,将其全部打上标记后,再次遍历矩阵的过程中,只对对未标记的’O’修改为’X’即可。
Code
class Solution:
def solve(self, board: List[List[str]]) -> None:
"""
Do not return anything, modify board in-place instead.
"""
n = len(board)
m = len(board[0])
def dfs(i, j):
if not 0 <= i < n or not 0 <= j < m or board[i][j] != "O" or board[i][j] == "A":
return
board[i][j] = 'A' # 将边界上的O标记为A
dfs(i-1, j)
dfs(i+1, j)
dfs(i, j-1)
dfs(i, j+1)
# 因为在边界上的“O”或与边界上的“O”相接的“O”都不算被“X”包围
# 所以直接对边界上的“O”进行dfs,找到所有与边界上的“O”相连接的"O",并标记
# 则在对边界上的“O”执行完dfs后,矩阵中没有被标记的“O”就是被“X”包围的“O”,反之被标记的‘O’就是未被“X”包围的“O”
# 对矩阵左右两边的的O执行dfs
for i in range(n):
dfs(i, 0)
dfs(i, m-1)
# 对矩阵上下两边上的O执行dfs
for j in range(m):
dfs(0, j)
dfs(n-1, j)
for i in range(n):
for j in range(m):
# 如果为A,则说明遇到了边界上的或与边界相接的O,不被X包围,还原为O
if board[i][j] == "A":
board[i][j] = "O"
# 反之则遇到了被X包围的O,修改为X
elif board[i][j] == "O":
board[i][j] = 'X'