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:
return
if len(path) == 2 * n:
res.append(path)
return
self.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():
continue
if 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)
return
self.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].importance
total = employees_dict[employee_id].importance # total变量存储员工重要度之和:初始化为当前遍历员工的重要度
for emp_id in employees_dict[employee_id].subordinates: # 如果当前员工有直系下属,则定位到当前员工的每个直系下属,继续DFS
total += self.dfs(employees_dict, emp_id) # 将遍历到的每个员工的重要度加到重要度之和
return total
def getImportance(self, employees: List['Employee'], id: int) -> int:
employees_dict = {}
for employee in employees:
employees_dict[employee.id] = employee
return 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":
return
grid[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 = 0
row, 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 1
if grid[x][y] == 2: # 2.函数因当前格子是已遍历的陆地格子,和周长没关系,返回0
return 0
if grid[x][y] != 1: # 3. 函数因当前格子是海洋格子,返回一条边
return 1
grid[x][y] = 2
return (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 0
if grid[x][y] != 1:
return 0
island_area = 1
grid[x][y] = 2
island_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_area
def maxAreaOfIsland(self, grid: List[List[int]]) -> int:
island_max_area = 0
row, col = len(grid), len(grid[0])
for x in range(row):
for y in range(col):
island_area = 0
if 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 0
grid[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 = 0
row, col = len(grid), len(grid[0])
for x in range(row):
for y in range(col):
island_area = 0
if grid[x][y] == 1:
island_area += self.dfs(grid, x, y)
island_max_area = max(island_max_area, island_area)
return island_max_area
第四题:飞地的数量
Leetcode1020. 飞地的数量:中等题 (详情点击链接见原题)
给你一个大小为
m x n
的二进制矩阵grid
,其中0
表示一个海洋单元格、1
表示一个陆地单元格。
一次 移动 是指从一个陆地单元格走到另一个相邻(上、下、左、右)的陆地单元格或跨过grid
的边界。
返回网格中 无法 在任意次数的移动中离开网格边界的陆地单元格的数量
解题思路
本题使用 DFS
、BFS
、并查集都是可以的,本题要求找不靠边的陆地面积,那么我们只要从周边找到陆地,然后通过 DFS
或者 BFS
将周边相邻的陆地都变成海洋,然后再去重新遍历地图的时候,统计此时还剩下的陆地就可以了
python代码解法:
class Solution:
def dfs(self, grid, x, y, visited):
for i, j in [(x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)]:
if 0 <= i < len(grid) and 0 <= j < len(grid[0]) and grid[i][j] == 1 and not visited[i][j]:
visited[i][j] = True
grid[i][j] = 0
self.dfs(grid, i, j, visited)
def numEnclaves(self, grid: List[List[int]]) -> int:
row, col = len(grid), len(grid[0])
visited = [[False for _ in range(col)] for _ in range(row)]
for r in range(row):
for c in range(col):
if r == 0 or r == row - 1 or c == 0 or c == col - 1:
if grid[r][c] == 1:
visited[r][c] = True
grid[r][c] = 0
self.dfs(grid, r, c, visited)
ans = 0
for r in range(row):
for c in range(col):
if grid[r][c] == 1:
ans += 1
return ans
第五题:被围绕的区域
Leetcode130. 被围绕的区域:中等题 (详情点击链接见原题)
给你一个
m x n
的矩阵board
,由若干字符'X'
和'O'
,找到所有被'X'
围绕的区域,并将这些区域里所有的'O'
用'X'
填充
解题思路
本题和上一题正好反过来了,上一题求得是地图中间的空格数,而本题是把地图中间的 O
都改成 X
依然是从地图周边出发,将周边空格相邻的 O
上都做上标记,然后再遍历一遍地图,遇到 O
且没做过标记的,那么都是地图中间的 O
,全部改为 X
就行
本题我们不定义 visited
数组,第一次遍历的时候利用深搜将地图周边的 O
全部改成 A
,在第二遍遍历地图,将 O
全部改成 X
,将 A
改回 O
python代码解法:
class Solution:
def dfs(self, grid, x, y):
for i, j in [(x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)]:
if 0 <= i < len(grid) and 0 <= j < len(grid[0]) and grid[i][j] == 'O':
grid[i][j] = 'A'
self.dfs(grid, i, j)
def solve(self, board: List[List[str]]) -> None:
row, col = len(board), len(board[0])
for r in range(row):
for c in range(col):
if r == 0 or r == row - 1 or c == 0 or c == col - 1:
if board[r][c] == 'O':
board[r][c] = 'A'
self.dfs(board, r, c)
for r in range(row):
for c in range(col):
if board[r][c] == 'A':
board[r][c] = 'O'
elif board[r][c] == 'O':
board[r][c] = 'X'
第六题:最大人工岛(待补充)
Leetcode827. 最大人工岛:困难题 (详情点击链接见原题)
给你一个大小为
n x n
二进制矩阵grid
。最多 只能将一格0
变成1
。
返回执行此操作后,grid
中最大的岛屿面积是多少?
python代码解法:
from typing import List
class Solution:
def __init__(self):
self.count = 0
def dfs(self, grid, x, y, visited, mark):
for r, c in [(x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)]:
if 0 <= r < len(grid) and 0 <= c < len(grid[0]):
if not visited[r][c] and grid[r][c] == 1:
visited[r][c] = True
grid[r][c] = mark # 标记为已访问
self.count += 1
self.dfs(grid, r, c, visited, mark)
def largestIsland(self, grid: List[List[int]]) -> int:
row, col = len(grid), len(grid[0])
visited = [[False for _ in range(col)] for _ in range(row)]
is_all_zero = True # 标记检查整个地图是否都为陆地
island_map = {0: 0}
mark = 2 # 记录每个岛屿的编号
for r in range(row):
for c in range(col):
self.count = 1
if grid[r][c] == 0:
is_all_zero = False
if not visited[r][c] and grid[r][c] == 1:
grid[r][c] = mark
visited[r][c] = True
self.dfs(grid, r, c, visited, mark)
island_map[mark] = self.count
mark += 1
if is_all_zero:
return row * col
ans = 0
visited_grid = set()
for i in range(row):
for j in range(col):
if grid[i][j] == 0:
count = 1
visited_grid.clear()
for x, y in [(i - 1, j), (i + 1, j), (i, j - 1), (i, j + 1)]:
if not (0 <= x < row and 0 <= y < col):
continue
if grid[x][y] in visited_grid:
continue
count += island_map[grid[x][y]]
visited_grid.add(grid[x][y])
ans = max(ans, count)
return ans
if __name__ == '__main__':
s = Solution()
grid = [[0, 0, 0, 0, 0, 0, 0],
[0, 1, 1, 1, 1, 0, 0],
[0, 1, 0, 0, 1, 0, 0],
[1, 0, 1, 0, 1, 0, 0],
[0, 1, 0, 0, 1, 0, 0],
[0, 1, 0, 0, 1, 0, 0],
[0, 1, 1, 1, 1, 0, 0]]
print(s.largestIsland(grid))
第七题:衣橱整理
Leetcode:衣橱整理:中等题 (详情点击链接见原题)
家居整理师将待整理衣橱划分为
m x n
的二维矩阵grid
,其中grid[i][j]
代表一个需要整理的格子。整理师自grid[0][0]
开始 逐行逐列 地整理每个格子
python代码解法:
class Solution:
def BitSum(self, num):
res = 0
while num > 0:
bit = num % 10
res += bit
num //= 10
return res
def 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 0
grid[x][y] = 2
return 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])):
return
if visited[x][y] or board[x][y] != word[index]: # 当前节点已访问或和word对应的字符不匹配
return
if index == len(word) - 1: # 当索引等于word的长度-1说明所有字符均匹配
return True
visited[x][y] = True # 标记为已访问
if self.dfs(board, x - 1, y, visited, word, index + 1):
return True
if self.dfs(board, x + 1, y, visited, word, index + 1):
return True
if self.dfs(board, x, y - 1, visited, word, index + 1):
return True
if self.dfs(board, x, y + 1, visited, word, index + 1):
return True
visited[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 True
return False
python代码解法:
class Solution:
def backtracking(self, visited, board, x, y, word, index):
if index == len(word):
return True
for r, c in [(x - 1, y), (x + 1, y), (x, y - 1), (x, y + 1)]:
if not (0 <= r < len(board) and 0 <= c < len(board[0])) or visited[r][c] or board[r][c] != word[index]:
continue
if board[r][c] == word[index]:
visited[r][c] = True # 标记为已访问
if self.backtracking(visited, board, r, c, word, index + 1):
return True
visited[r][c] = False
return 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 x in range(row):
for y in range(col):
if board[x][y] == word[0]:
visited[x][y] = True
if self.backtracking(visited, board, x, y, word, 1):
return True
visited[x][y] = False
return False
if __name__ == '__main__':
s = Solution()
board = [["a","a"]]
word = "aaa"
print(s.exist(board, word))
第九题:串联字符串的最大长度
Leetcode1239. 串联字符串的最大长度:中等题 (详情点击链接见原题)
给定一个字符串数组
arr
,字符串s
是将arr
的含有 不同字母 的 子序列 字符串 连接 所得的字符串
三、记忆化递归
第一题:单词拆分
Leetcdoe139. 单词拆分:中等题 (详情点击链接见原题)
给你一个字符串
s
和一个字符串列表wordDict
作为字典。如果可以利用字典中出现的一个或多个单词拼接出s
则返回true
python代码解法:
class Solution:
def backtracking(self, s: str, wordSet: set[str], memory, startIndex: int) -> bool:
# 边界情况:已经遍历到字符串末尾,返回True
if startIndex >= len(s):
return True
if 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):
# 如果截取的子串在字典中,并且后续部分也可以被拆分成单词,返回True
return True
memory[startIndex] = 0
# 无法进行有效拆分,返回False
return False
def 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 # 每个节点的初始路径为 1
for 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] = ans
return ans
def 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