为了方便以后回顾进行汇总,目前总结了DFS基于栈的非递归写法、递归写法,以及DFS+回溯三类。
第一类(非递归+栈)
Leetcode 200. 岛屿数量
题目描述:给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。
岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。
此外,你可以假设该网格的四条边均被水包围。
示例 1:
输入:grid = [
["1","1","1","1","0"],
["1","1","0","1","0"],
["1","1","0","0","0"],
["0","0","0","0","0"]
]
输出:1
示例 2:
输入:grid = [
["1","1","0","0","0"],
["1","1","0","0","0"],
["0","0","1","0","0"],
["0","0","0","1","1"]
]
输出:3
提示:
m == grid.length
n == grid[i].length
1 <= m, n <= 300
grid[i][j] 的值为 '0' 或 '1'
思路
基于DFS的解题思路,可以将二维数组看成一个无向图,水平/竖直方向上相邻的1间有边相连。算法最外层遍历二维数组,如果当前元素为1,则基于当前节点进行DFS,即将与该元素水平/竖直相邻的1元素的位置添加至栈中。因为每发现一个1元素(即发现一个岛屿)就会进行一次DFS,所以此题中岛屿的数量实际上就是进行DFS的次数。
此外,还有类似题目——Leetcode 695. 岛屿的最大面积。这两题的思路是一样的,只不过求最大面积时相当于每完成一次DFS,需要将当前岛屿的面积与之前的最大面积进行比较,即 area = max(area, cur_area)。
Python3代码
- 时间复杂度:。其中是给定网格中的行数,是列数。每个网格最多访问一次。
- 空间复杂度:。栈中最多会存放所有的土地,所以土地的数量最多为块,因此使用的空间为。
class Solution:
def numIslands(self, grid: List[List[str]]) -> int:
cnt = 0
length = len(grid)
width = len(grid[0])
for i in range(length):
for j in range(width):
if grid[i][j]: # if 当前元素为1
visited = [(i, j)] # 存储待访问元素位置
grid[i][j] = 0 # 访问过的1元素置为0,防止重复访问
cnt += 1
else:
visited = []
while visited: # 进行一次DFS
x, y = visited.pop() # pop()会返回list当前最后一个元素,利用list模拟stack的 Last In First Out 来实现DFS
for di in [(x+1, y), (x-1, y), (x, y-1), (x, y+1)]:
if 0 <= di[0] < length and 0 <= di[1] < width and grid[di[0]][di[1]] == 1:
visited.append((di[0], di[1]))
grid[di[0]][di[1]] = 0
return cnt
第二类(递归)
Leetcode 547. 省份数量
题目描述:有 n 个城市,其中一些彼此相连,另一些没有相连。如果城市 a 与城市 b 直接相连,且城市 b 与城市 c 直接相连,那么城市 a 与城市 c 间接相连。
省份 是一组直接或间接相连的城市,组内不含其他没有相连的城市。
给你一个 n x n 的矩阵 isConnected ,其中 isConnected[i][j] = 1 表示第 i 个城市和第 j 个城市直接相连,而 isConnected[i][j] = 0 表示二者不直接相连。
返回矩阵中 省份 的数量。
示例 1:
输入:isConnected = [[1,1,0],[1,1,0],[0,0,1]]
输出:2
示例 2:
输入:isConnected = [[1,0,0],[0,1,0],[0,0,1]]
输出:3
提示:
1 <= n <= 200
n == isConnected.length
n == isConnected[i].length
isConnected[i][j] 为 1 或 0
isConnected[i][i] == 1
isConnected[i][j] == isConnected[j][i]
思路
本题与Leetcode 200. 岛屿数量的较大不同点,一个是在岛屿数量中,我们需要自行判断矩阵中两个元素是否相连,而本题中,矩阵的元素直接告诉了我们两个元素是否相连;此外,在岛屿数量中,我们需要通过(i, j)两个变量才能确定一个元素,且i和j本身是两个不同的变量,而本题中的二维矩阵,实际上是n*n的图邻接矩阵(n为城市数量),所以i和j实际上指向的是同一个变量(即城市)。所以如果要基于栈实现本题的话,添加进栈的元素应该只有一个,即待访问的城市j,而非i和j两个。
Python3代码
- 时间复杂度:。其中是城市的数量,因为需要遍历矩阵中的每个元素,可以理解为 。
- 空间复杂度:。因为需要使用数组记录每个城市是否被访问过,数组长度是,递归调用栈的深度不会超过。
class Solution:
def findCircleNum(self, isConnected: List[List[int]]) -> int:
def dfs(i):
for j in range(n):
if isConnected[i][j] and j not in visited: # 知道所有j都访问过时,结束递归
visited.add(j)
dfs(j)
cnt = 0
n = len(isConnected) # 因为是对称矩阵,所以为正方形,即n*n
visited = set() # 待访问城市,用set避免重复访问
for i in range(n):
if i not in visited:
dfs(i)
cnt += 1 # 一次DFS即为一个省份
return cnt
第三类(DFS+回溯)
Leetcode 797. 所有可能的路径
题目描述:给你一个有 n 个节点的 有向无环图(DAG),请你找出所有从节点 0 到节点 n-1 的路径并输出(不要求按特定顺序)
graph[i] 是一个从节点 i 可以访问的所有节点的列表(即从节点 i 到节点 graph[i][j]存在一条有向边)。
示例 1:
输入:graph = [[1,2],[3],[3],[]]
输出:[[0,1,3],[0,2,3]]
解释:有两条路径 0 -> 1 -> 3 和 0 -> 2 -> 3
示例 2:
输入:graph = [[4,3,1],[3,2,4],[3],[4],[]]
输出:[[0,4],[0,3,4],[0,1,3,4],[0,1,2,3,4],[0,1,4]]
提示:
n == graph.length
2 <= n <= 15
0 <= graph[i][j] < n
graph[i][j] != i(即不存在自环)
graph[i] 中的所有元素 互不相同
保证输入为 有向无环图(DAG)
思路
首先先看看度娘对于回溯法的解释——回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。再对应回本题,也就是从节点 0 开始往前走,如果最后能走到节点 n - 1,则返回其路径;如果不能走到(因本题的图为有向无环图,所以不会出现无限循环的bug),则退回至起点,换条路接着试。到这里,可以发现这个解题思路对路径的深入方式与DFS不谋而合。
Python3代码
- 时间复杂度:。其中为图中点的数量。我们可以找到一种最坏情况,即每一个点都可以去往编号比它大的点。此时路径数为,且每条路径长度为,因此总时间复杂度为。
- 空间复杂度:。主要为栈空间的开销。
class Solution:
def allPathsSourceTarget(self, graph: List[List[int]]) -> List[List[int]]:
result = []
path = [0] # 起始节点 0
def dfs(i):
if i == len(graph) - 1: # path已经走到了节点 n - 1
result.append(path[:])
return
for j in graph[i]:
path.append(j)
dfs(j)
path.pop() # 回溯
dfs(0)
return result
79. 单词搜索
题目描述:给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。
示例 1:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true
示例 2:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "SEE"
输出:true
示例 3:
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCB"
输出:false
提示:
m == board.length
n = board[i].length
1 <= m, n <= 6
1 <= word.length <= 15
board 和 word 仅由大小写英文字母组成
思路
基于DFS,利用额外的用来保存已经访问过的元素位置,防止重复访问。如果元素符合要求则接着DFS,不符合就回溯。此外因为这题只需要判断True / False,所以其实可以不记录具体的路径。
Python3代码
class Solution:
def exist(self, board: List[List[str]], word: str) -> bool:
m = len(board[0])
n = len(board)
def dfs(visited, x, y, index) -> bool:
result = False
# path.append(board[x][y]) # 因为这道题并不关注path,所以可以不需要存path
visited.add((x, y))
if index == len(word):
return True
for di in [(x-1, y), (x+1, y), (x, y-1), (x, y+1)]:
if 0 <= di[0] < n and 0 <= di[1] < m and board[di[0]][di[1]] == word[index] and (di[0], di[1]) not in visited:
if dfs(visited, di[0], di[1], index + 1):
result = True
break # 已经找到了word,所以不需要继续DFS了
# 回溯visited和path
visited.remove((x, y))
# path.pop()
return result
visited = set()
for i in range(n):
for j in range(m):
if board[i][j] == word[0]: # 找到了开头的字母,以该字母为起点进行DFS
# path = []
if dfs(visited, i, j, 1):
return True
return False