其他分享
首页 > 其他分享> > 【回溯】力扣79:单词搜索

【回溯】力扣79:单词搜索

作者:互联网

给定一个 m x n 二维字符网格 board 和一个字符串单词 word 。如果 word 存在于网格中,返回 true ;否则,返回 false 。

单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中“相邻”单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母不允许被重复使用。

示例:

image
输入:board = [["A","B","C","E"],["S","F","C","S"],["A","D","E","E"]], word = "ABCCED"
输出:true

看到搜索,想到 DFS,从而考虑是否需要回溯

看到相邻,想到 方向数组directions = [(0, 1), (0, -1), (1, 0), (-1, 0)]

此题为选择类问题。对于已访问的路径标记,可以维护一个 visited 的dict(tuple),先标记当前位置为已访问,以避免重复遍历(如防止向右搜索后又向左返回);在所有的可能都搜索完成后,再回改当前位置为未访问,防止干扰其它位置搜索到当前位置。


  1. 外层:遍历
    首先遍历给定网格 board 的所有元素,先找到和单词 word 第一个字母相同的元素,然后进入递归流程。

    在外层对每一个位置 (i, j) 都调用函数 check(i, j, 0) 进行检查:只要有一处返回 true,就说明网格中能够找到相应的单词,否则说明不能找到,返回 false

  2. 内层:递归
    定义搜索函数 check(x, y, index) 来判断从 (x, y) 出发,能否搜索到单词 word[index..],其中 word[index..] 表示字符串 word 从第 index 个字符开始的后缀子串。如果能搜索到,则返回 true,反之返回 false。

    函数 check(x, y, index)的执行步骤如下:

    • 如果 board[x][y] ≠ word[index],当前字符不匹配,直接返回 false

    • 如果当前已经访问到字符串的末尾 且对应字符依然匹配 (index == len(word) - 1),此时直接返回 true;否则,朝它的上下左右试探,看看它周边的这四个元素是否能匹配 word 的下一个字母

      • 如果匹配:带着该元素继续进入下一个递归
      • 如果上下左右全部都不匹配:返回 false
  3. 注意点

有标记数组

标记数组 visited 可以初始化为空,遇到一个结点就加进来,表示已访问,如果相邻路径都走不通再踢出去

class Solution:
    def exist(self, board: List[List[str]], word: str) -> bool:
        n = len(word)
        if n == 0:
            return True
        if not board and word:
            return False

        row, col = len(board), len(board[0])
        directions = [(0, 1), (0, -1), (1, 0), (-1, 0)] # 方向数组,里面用小括号中括号不影响判断
        visited = set() # 已访问数组

        def check(x, y, index):
            if board[x][y] != word[index]: # 一旦不匹配就返回false
                return False
            if index == n - 1: # 已经遍历到单词末尾还匹配,说明全部匹配,返回true
                return True
            visited.add((x, y)) # 不满足第一个判断条件就说明匹配,将该结点标记为已访问
            for dx, dy in directions:
                nx, ny = x + dx, y + dy
                if 0 <= nx <row and 0 <= ny < col:
                    if (nx, ny) not in visited and check(nx, ny, index + 1):
                        return True
                        break
            visited.remove((x, y))
            return False

        for i in range(row):
            for j in range(col):
                if check(i, j, 0):
                    return True
        return False

标记数组 visited 也可以先初始化为与 board 等大的数组,发现当前访问结点与字符串第一个元素匹配就标记为已访问visited[i][j] = 1,如果递归发现不完全匹配再取消标记visited[i][j] = 0

class Solution:
    def exist(self, board: List[List[str]], word: str) -> bool:
        n = len(word)
        if n == 0:
            return True
        if not board and word:
            return False

        row, col = len(board), len(board[0])
        directions = [(0, 1), (0, -1), (1, 0), (-1, 0)] # 方向数组,里面用小括号中括号不影响判断
        visited = [[0 for _ in range(col)] for _ in range(row)] # 与 board 等大的已访问数组

        def check(x, y, index):
            if board[x][y] != word[index]: # 一旦不匹配就返回false
                return False
            if index == n - 1: # 已经遍历到单词末尾还匹配,说明全部匹配,返回true
                return True
            for dx, dy in directions:
                nx, ny = x + dx, y + dy
                if 0 <= nx <row and 0 <= ny < col:
                    if visited[nx][ny] == 1: # 如果是已访问的元素就忽略
                        continue
                    visited[nx][ny] = 1 # 如果是未访问的元素就标记为已访问
                    if check(nx, ny, index + 1):
                        return True
                    else: # 回溯
                        visited[nx][ny] = 0
            return False

        for i in range(row):
            for j in range(col):
                if board[i][j] == word[0]: # 如果与字符串第一个元素匹配就标记为已访问
                    visited[i][j] = 1
                    if check(i, j, 0):
                        return True
                    else: # 回溯
                        visited[i][j] = 0
        return False

时间复杂度:一个非常宽松的上界为 O(MN⋅3^L)
),其中 M, N 为网格的长度与宽度,L 为字符串 word 的长度。在每次调用函数 check 时,除了第一次可以进入 4 个分支以外,其余时间最多会进入 3 个分支(因为每个位置只能使用一次,所以走过来的分支没法走回去)。由于单词长为 L,故 check(i,j,0) 的时间复杂度为 O(3^L),但是需要执行 O(MN) 次检查。然而,由于剪枝的存在,在遇到不匹配或已访问的字符时会提前退出,终止递归流程。因此,实际的时间复杂度会远远小于 Θ(MN⋅3^L)。

空间复杂度:O(MN)。额外开辟了 O(MN) 的 visited 数组,同时栈的深度最大为 O(min(L,MN))。

无标记数组

题目没有明确说不能修改原数组,那么修改原数组不失为更简洁省空间的方式。

在辅函数中,如果 board[x][y] 匹配了 word[index],就标记该结点为已访问状态board[x][y] = '#',避免反过来再检测。然后对相邻位置进行递归查找,如果周边结点都检测一遍都为false了,再将这个结点还原board[x][y] = word[index],接着到下一个结点比较。

class Solution:
    def exist(self, board: List[List[str]], word: str) -> bool:
        n = len(word)
        if n == 0:
            return True
        if not board and word:
            return False

        row, col = len(board), len(board[0])
        directions = [[0,1], [0,-1], [1,0], [-1,0]] # 方向数组,里面用小括号中括号不影响判断

        def check(x, y, index):
            if board[x][y] != word[index]: # 一旦不匹配就返回false
                return False
            if index == n - 1: # 已经遍历到单词末尾还匹配,说明全部匹配,返回true
                return True
            board[x][y] = '#' # 标记为已访问
            for direct in directions:
                nx, ny = x + direct[0], y + direct[1] # 注意这里是derect,不是directions
                if 0 <= nx <row and 0 <= ny < col and check(nx, ny, index + 1):
                    return True
            board[x][y] = word[index] # 如果路径走不通,取消标记,还原结点

        for i in range(row):
            for j in range(col):
                if check(i, j, 0):
                    return True
        return False

标签:index,word,return,len,力扣,board,回溯,匹配,79
来源: https://www.cnblogs.com/Jojo-L/p/16582713.html