DFS(深度优先搜索)做题笔记

本文详细介绍了深度优先搜索(DFS)在解决Acwing和LeetCode中的经典问题,包括排列数字、N皇后问题、二叉树操作(如最大深度、最小深度、路径和)、岛屿问题(如面积、周长和数量)以及二维矩阵操作。通过代码示例展示了如何运用DFS算法解决问题并提供状态管理技巧。
摘要由CSDN通过智能技术生成

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'

未完结,以后做到会更新

  • 24
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值