算法刷题自记录 | Leetcode200. 岛屿数量,547. 省份数量,797. 所有可能的路径,79. 单词搜索(DFS题型汇总)

  为了方便以后回顾进行汇总,目前总结了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的解题思路,可以将二维数组grid看成一个无向图,水平/竖直方向上相邻的1间有边相连。算法最外层遍历二维数组,如果当前元素为1,则基于当前节点进行DFS,即将与该元素水平/竖直相邻的1元素的位置添加至栈中。因为每发现一个1元素(即发现一个岛屿)就会进行一次DFS,所以此题中岛屿的数量实际上就是进行DFS的次数。

  此外,还有类似题目——Leetcode 695. 岛屿的最大面积。这两题的思路是一样的,只不过求最大面积时相当于每完成一次DFS,需要将当前岛屿的面积与之前的最大面积进行比较,即 area = max(area, cur_area)。

Python3代码

  • 时间复杂度:O(N*M)。其中N是给定网格中的行数,M是列数。每个网格最多访问一次。
  • 空间复杂度:O(N*M)。栈中最多会存放所有的土地,所以土地的数量最多为N*M块,因此使用的空间为O(N*M)
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代码

  • 时间复杂度:O(N^2)。其中N是城市的数量,因为需要遍历矩阵中的每个元素,可以理解为 。
  • 空间复杂度:O(N^2)。因为需要使用数组visited记录每个城市是否被访问过,数组长度是N,递归调用栈的深度不会超过N
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,则返回其路径path;如果不能走到(因本题的图为有向无环图,所以不会出现无限循环的bug),则退回至起点,换条路接着试。到这里,可以发现这个解题思路对路径的深入方式与DFS不谋而合。

Python3代码

  • 时间复杂度:O(N*2^N)。其中N为图中点的数量。我们可以找到一种最坏情况,即每一个点都可以去往编号比它大的点。此时路径数为O(2^N),且每条路径长度为O(N),因此总时间复杂度为O(N*2^N)
  • 空间复杂度:O(N)。主要为栈空间的开销。
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,利用额外的visited用来保存已经访问过的元素位置,防止重复访问。如果元素符合要求则接着DFS,不符合就回溯。此外因为这题只需要判断True / False,所以其实可以不记录具体的路径path

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
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值