Leetcode刷题笔记——DFS篇
一、二叉树DFS的相关应用
第一题:括号生成
Leetcode22:括号生成:中等题 (详情点击链接见原题)
数字
n
代表生成括号的对数,请你设计一个函数,用于能够生成所有可能的并且 有效的括号组合
本质就是一个二叉树,,二叉树的每个节点即一种括号组合,分别递归的加入左括号
python代码解法1:
方便更直观的看到回溯的过程
class Solution:def dfs(self, n, left, right, path, result):if len(path) == 2 * n: # 递归出口result.append("".join(path[:]))if left < n:path.append('(')self.dfs(n, left + 1, right, path, result)path.pop(-1)if right < left:path.append(')')self.dfs(n, left, right + 1, path, result)path.pop(-1)def generateParenthesis(self, n: int) -> List[str]:path, result = [], []self.dfs(n, 0, 0, path, result)return result
python代码解法2:
简单写法
class Solution:def dfs(self, n, path, res, left, right):if left > n or right > n or right > left:returnif len(path) == 2 * n:res.append(path)returnself.dfs(n, path + '(', res, left + 1, right)self.dfs(n, path + ')', res, left, right + 1)def generateParenthesis(self, n: int) -> List[str]:path, res = '', []self.dfs(n, path, res, 0, 0)return res
第二题:字母大小写全排列
Leetcode784:字母大小写全排列:中等题 (详情点击链接见原题)
给定一个字符串
s
,通过将字符串s
中的每个字母转变大小写,我们可以获得一个新的字符串。
返回 所有可能得到的字符串集合 。以 任意顺序返回输出
python代码解法1:
class Solution:def backtracking(self, s, index, result):result.append("".join(s))for i in range(index, len(s)):if s[i].isdigit():continueif s[i].isalpha():if s[i].islower():s[i] = chr(ord(s[i]) - 32)self.backtracking(s, i + 1, result)s[i] = chr(ord(s[i]) + 32)else:s[i] = chr(ord(s[i]) + 32)self.backtracking(s, i + 1, result)s[i] = chr(ord(s[i]) - 32)def letterCasePermutation(self, s: str) -> List[str]:result = []s1 = list(s)self.backtracking(s1, 0, result)return result
python代码解法2:
class Solution:def dfs(self, s, index, path, res):if index == len(s):res.append(path)returnself.dfs(s, index + 1, path + s[index], res)if s[index].isalpha():if s[index].islower():self.dfs(s, index + 1, path + chr(ord(s[index]) - 32), res)else:self.dfs(s, index + 1, path + chr(ord(s[index]) + 32), res)def letterCasePermutation(self, s: str) -> List[str]:path, res = "", []st = list(s)self.dfs(st, 0, path, res)return res
第三题:为运算表达式设计优先级
Leetcode241. 为运算表达式设计优先级:中等题 (详情点击链接见原题)
给你一个由数字和运算符组成的字符串
expression
,按不同优先级组合数字和运算符,计算并返回所有可能组合的结果
解题思路
对于一个形如 x op y
【op
为运算符,x
和 y
为操作数】的算式而言,它的结果组合取决于x
和 y
的结果组合数,而 x
和 y
又可以递归的看成x op y
的算式,该问题的子问题就是 x op y
中的 x
和 y
:以运算符分隔的左右两侧算式解
- 分解:按运算符分成左右两部分,分别求解
- 解决:实现一个递归函数,输入算式,返回算式解
- 合并:根据运算符合并左右两部分的解,得出最终解
python代码解法:
class Solution:def diffWaysToCompute(self, expression: str) -> List[int]:if expression.isdigit(): # 1. 如果只有数字,直接返回return [int(expression)]res = []for i, char in enumerate(expression):# 1.分解:遇到运算符,计算左右两侧的结果集# 2.解决:diffWaysToCoumpute 递归函数求出子问题的解left = self.diffWaysToCompute(expression[:i])right = self.diffWaysToCompute(expression[i + 1:])# 3.合并:根据运算符合并子问题的解for l in left:for r in right:if char == '+':res.append(l + r)elif char == '-':res.append(l - r)else:res.append(l * r)return res
第四题:员工的重要性
Letcode690. 员工的重要性:中等题 (详情点击链接见原题)
给定一个保存员工信息的数据结构,它包含了员工 唯一的
id
,重要度 和 直系下属的id
解题思路
所有员工形成多叉树的结构,每个员工对应多叉树中的一个节点,每个节点包含【员工编号、重要度、直系下属的编号】,一个员工的直系下属对应多叉树中的一个结点的子结点
- 对于给定的整数
id
,计算以该整数编号对应的员工为根节点的子树中的所有结点的员工重要度之和 - 首先遍历数组
employees
并使用哈希表记录{员工id: 对应 id 的员工信息}
- 定位到
整数id
对应的员工,计算以该员工为根节点的子树中所有结点的员工重要度之和
python代码解法:
class Solution:def dfs(self, employees_dict, employee_id):if not employees_dict[employee_id].subordinates: # 递归出口:当某员工没有直系下属时,返回自身的重要度return employees_dict[employee_id].importancetotal = employees_dict[employee_id].importance # total变量存储员工重要度之和:初始化为当前遍历员工的重要度for emp_id in employees_dict[employee_id].subordinates: # 如果当前员工有直系下属,则定位到当前员工的每个直系下属,继续DFStotal += self.dfs(employees_dict, emp_id) # 将遍历到的每个员工的重要度加到重要度之和return totaldef getImportance(self, employees: List['Employee'], id: int) -> int:employees_dict = {}for employee in employees:employees_dict[employee.id] = employeereturn self.dfs(employees_dict, id)
二、网格(岛屿)问题中的DFS
岛屿问题是一类典型的网格问题,每个格子中的数字可能是 0
或者 1
。我们把数字为 0
的格子看成海洋格子,数字为 1
的格子看成陆地格子,这样相邻的陆地格子就连接成一个岛屿,在这样一个设定下,就出现了各种岛屿问题的变种,包括岛屿的数量、面积、周长等。不过这些问题,基本都可以用 DFS
遍历来解决
在二叉树的 DFS
中有两个要素:「访问相邻结点」和「判断 base case」
访问相邻结点
二叉树本身就是一个递归定义的结构:一棵二叉树,它的左子树和右子树也是一棵二叉树。那么我们的 DFS
遍历只需要递归调用左子树和右子树即可
网格结构中的格子有多少相邻结点?。对于格子 (r, c)
来说(r
和 c
分别代表行坐标和列坐标),四个相邻的格子分别是 (r-1, c)、(r+1, c)、(r, c-1)、(r, c+1)
。换句话说,网格结构是「四叉」的。
判断 base case
二叉树遍历的 base case
是 root == null
网格遍历的 base case
是 判断当前网格的坐标是否超出网格范围,超出则无需遍历
网格结构 DFS
中如何避免重复遍历
网格结构的 DFS
与二叉树的 DFS
最大的不同之处在于,遍历中可能遇到遍历过的结点,我们可以把每个格子看成图中的结点,每个结点有向上下左右的四条边。在图中遍历时,自然可能遇到重复遍历结点,这时候 DFS
可能会不停地「兜圈子」,永远停不下来,解决方案是:标记已经遍历过的格子
第一题:岛屿数量
Leetcode200:岛屿数量:中等题 (详情点击链接见原题)
给你一个由
'1'
(陆地)和'0'
(水)组成的的二维网格,请你计算网格中岛屿的数量。岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。此外,你可以假设该网格的四条边均被水包围
python代码解法:
class Solution:def dfs(self, grid, x, y):if not (0 <= x < len(grid) and 0 <= y < len(grid[0])) or grid[x][y] != "1":returngrid[x][y] = "2" # 标记为已访问self.dfs(grid, x - 1, y)self.dfs(grid, x + 1, y)self.dfs(grid, x, y - 1)self.dfs(grid, x, y + 1)def numIslands(self, grid: List[List[str]]) -> int:island_num = 0row, col = len(grid), len(grid[0])for x in range(row):for y in range(col): # 1. 循环遍历网格中的所有节点if grid[x][y] == "1":island_num += 1 # 2. DFS遍历的次数即网格中岛屿的数量self.dfs(grid, x, y)return island_num
第二题:岛屿的周长
Leetcode463:岛屿的周长:简单题 (详情点击链接见原题)
给定一个
row x col
的二维网格地图grid
,其中:grid[i][j] = 1
表示陆地,grid[i][j] = 0
表示水域
python代码解法:
class Solution:def dfs_perimeter(self, x, y, grid):if not (0 <= x < len(grid) and 0 <= y < len(grid[0])): # 1.函数因坐标(x, y)超出边界范围,返回一条边return 1if grid[x][y] == 2: # 2.函数因当前格子是已遍历的陆地格子,和周长没关系,返回0return 0if grid[x][y] != 1: # 3. 函数因当前格子是海洋格子,返回一条边return 1grid[x][y] = 2return (self.dfs_perimeter(x + 1, y, grid) + self.dfs_perimeter(x - 1, y, grid) +self.dfs_perimeter(x, y + 1, grid) + self.dfs_perimeter(x, y - 1, grid))def islandPerimeter(self, grid: List[List[int]]) -> int:row, col = len(grid), len(grid[0])for x in range(row):for y in range(col):if grid[x][y] == 1:return self.dfs_perimeter(x, y, grid)
第三题:岛屿的最大面积
Leetcode695:岛屿的最大面积:中等题 (详情点击链接见原题)
给你一个大小为
m x n
的二进制矩阵grid
。
岛屿 是由一些相邻的1
(代表土地) 构成的组合,这里的「相邻」要求两个1
必须在 水平或者竖直的四个方向上 相邻。你可以假设grid
的四个边缘都被0
(代表水)包围着
python代码解法:
class Solution:def dfs_count(self, x, y, grid, island_area):row, col = len(grid), len(grid[0])if x < 0 or x >= row or y < 0 or y >= col:return 0if grid[x][y] != 1:return 0island_area = 1grid[x][y] = 2island_area += self.dfs_count(x + 1, y, grid, island_area)island_area += self.dfs_count(x - 1, y, grid, island_area)island_area += self.dfs_count(x, y + 1, grid, island_area)island_area += self.dfs_count(x, y - 1, grid, island_area)return island_areadef maxAreaOfIsland(self, grid: List[List[int]]) -> int:island_max_area = 0row, col = len(grid), len(grid[0])for x in range(row):for y in range(col):island_area = 0if grid[x][y] == 1:island_area += self.dfs_count(x, y, grid, island_area)island_max_area = max(island_max_area, island_area)return island_max_area
python代码解法(优化后):
class Solution:def dfs(self, grid, x, y):if not (0 <= x < len(grid) and 0 <= y < len(grid[0])) or grid[x][y] != 1:return 0grid[x][y] = 2 # 标记为已访问return 1 + self.dfs(grid, x + 1, y) + self.dfs(grid, x - 1, y) + self.dfs(grid, x, y - 1) + self.dfs(grid, x, y + 1)def maxAreaOfIsland(self, grid: List[List[int]]) -> int:island_max_area = 0row, col = len(grid), len(grid[0])for x in range(row):for y in range(col):island_area = 0if grid[x][y] == 1:island_area += self.dfs(grid, x, y)island_max_area = max(island_max_area, island_area)return island_max_area
第四题:最大人工岛(待补充)
Leetcode827. 最大人工岛:困难题 (详情点击链接见原题)
给你一个大小为
n x n
二进制矩阵grid
。最多 只能将一格0
变成1
。
返回执行此操作后,grid
中最大的岛屿面积是多少?
第五题:衣橱整理
Leetcode:衣橱整理:中等题 (详情点击链接见原题)
家居整理师将待整理衣橱划分为
m x n
的二维矩阵grid
,其中grid[i][j]
代表一个需要整理的格子。整理师自grid[0][0]
开始 逐行逐列 地整理每个格子
python代码解法:
class Solution:def BitSum(self, num):res = 0while num > 0:bit = num % 10res += bitnum //= 10return resdef dfs(self, grid, x, y, cnt):if not (0 <= x < len(grid) and 0 <= y < len(grid[0]) and self.BitSum(x) + self.BitSum(y) <= cnt and grid[x][y] == 0):return 0grid[x][y] = 2return 1 + self.dfs(grid, x - 1, y, cnt) + self.dfs(grid, x + 1, y, cnt) + self.dfs(grid, x, y - 1, cnt) + self.dfs(grid, x, y + 1, cnt)def wardrobeFinishing(self, m: int, n: int, cnt: int) -> int:grid = [[0 for _ in range(n)] for _ in range(m)]count = self.dfs(grid, 0, 0, cnt)return count
第六题:单词搜索
Leetcode79:单词搜索:中等题 (详情点击链接见原题)
给定一个
m x n
二维字符网格board
和一个字符串单词word
。如果word
存在于网格中,返回true
;否则,返回false
。
仔细观察即可发现该题与我们上文中的Leetcode112:路径总和的解题思路有点类似
解题思路
- 确定递归函数和的参数和返回值:
- 确定终止条件:当找到最终结果即
index == len(word) - 1
就要返回 - 确定单层递归测逻辑:当搜索范围超出网格边界或者是已访问过的节点,或者是当前访问的节点不等于单词中对应的字符,则应立即返回
python代码解法:
class Solution:def dfs(self, board, x, y, visited, word, index):if not (0 <= x < len(board) and 0 <= y < len(board[0])):returnif visited[x][y] or board[x][y] != word[index]: # 当前节点已访问或和word对应的字符不匹配returnif index == len(word) - 1: # 当索引等于word的长度-1说明所有字符均匹配return Truevisited[x][y] = True # 标记为已访问if self.dfs(board, x - 1, y, visited, word, index + 1):return Trueif self.dfs(board, x + 1, y, visited, word, index + 1):return Trueif self.dfs(board, x, y - 1, visited, word, index + 1):return Trueif self.dfs(board, x, y + 1, visited, word, index + 1):return Truevisited[x][y] = False # 回溯,清除访问标识def exist(self, board: List[List[str]], word: str) -> bool:row, col = len(board), len(board[0])visited = [[False for _ in range(col)] for _ in range(row)]for r in range(row):for c in range(col):if board[r][c] == word[0]:if self.dfs(board, r, c, visited, word, 0):return Truereturn False
第八题:串联字符串的最大长度
Leetcode1239. 串联字符串的最大长度:中等题 (详情点击链接见原题)
给定一个字符串数组
arr
,字符串s
是将arr
的含有 不同字母 的 子序列 字符串 连接 所得的字符串
三、记忆化递归
第一题:单词拆分
Leetcdoe139. 单词拆分:中等题 (详情点击链接见原题)
给你一个字符串
s
和一个字符串列表wordDict
作为字典。如果可以利用字典中出现的一个或多个单词拼接出s
则返回true
python代码解法:
class Solution:def backtracking(self, s: str, wordSet: set[str], memory, startIndex: int) -> bool:# 边界情况:已经遍历到字符串末尾,返回Trueif startIndex >= len(s):return Trueif memory[startIndex] != 1:return memory[startIndex]# 遍历所有可能的拆分位置for i in range(startIndex, len(s)):word = s[startIndex:i + 1] # 截取子串if word in wordSet and self.backtracking(s, wordSet, memory, i + 1):# 如果截取的子串在字典中,并且后续部分也可以被拆分成单词,返回Truereturn Truememory[startIndex] = 0# 无法进行有效拆分,返回Falsereturn Falsedef wordBreak(self, s: str, wordDict: List[str]) -> bool:wordSet = set(wordDict) # 转换为哈希集合,提高查找效率memory = [1] * len(s)return self.backtracking(s, wordSet, memory, 0)
第二题:矩阵中的最长递增路径
Leetcode329. 矩阵中的最长递增路径:困难题 (详情点击链接见原题)
定一个
m x n
整数矩阵matrix
,找出其中 最长递增路径 的长度
解题思路
从每一个点出发,往下深搜,看它最远能到哪
class Solution:def dfs(self, matrix, x, y, memo):if memo[x][y] != 0: # 已经遍历过的直接返回return memo[x][y]ans = 1 # 每个节点的初始路径为 1for i, j in [(x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)]:if 0 <= i < len(matrix) and 0 <= j < len(matrix[0]) and matrix[x][y] < matrix[i][j]: # 看四个方向是否有满足条件的节点去扩散ans = max(ans, self.dfs(matrix, i, j, memo) + 1)memo[x][y] = ansreturn ansdef longestIncreasingPath(self, matrix: List[List[int]]) -> int:m, n = len(matrix), len(matrix[0])memo = [[0 for _ in range(n)] for _ in range(m)] # memo数组用来对已经遍历过节点的最长递增路径进行记忆(防止重复计算)res = 0# 1.每个点都要作为起始点遍历一下for r in range(m):for c in range(n):if memo[r][c] == 0: # 2.已经遍历过的就不用遍历了res = max(res, self.dfs(matrix, r, c, memo))# print(memo) # 大家可以在纸上推算一下memo数组的结果,最终打印出来对比看是否一致return res