单词搜索

标签: 数组 字符串 回溯 矩阵

难度: Medium

给定一个 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
  • boardword 仅由大小写英文字母组成

 

进阶:你可以使用搜索剪枝的技术来优化解决方案,使其在 board 更大的情况下可以更快解决问题?

Submission

运行时间: 204 ms

内存: 17.1 MB

class Solution:
    def exist(self, board: List[List[str]], word: str) -> bool:
        m = len(board)
        n = len(board[0])

        def backtrack(path, visited, i, j):
            if len(path) == len(word):
                return True
            
            choices = [(i-1, j), (i+1, j), (i, j-1), (i, j+1)]

            for choice in choices:
                x, y = choice
                if (x, y) in visited or x < 0 or y < 0 or x >= m or y >= n or word[len(path)] != board[x][y]:
                    continue
                visited.add((x, y))
                path.append(board[x][y])
                if backtrack(path, visited, x, y):
                    return True
                path.pop()
                visited.remove((x, y))


        for row in range(m):
            for col in range(n):
                if board[row][col] == word[0]:
                    if backtrack([word[0]], {(row, col)}, row, col):
                        return True
        return False

Explain

这个题解使用回溯法来解决单词搜索问题。从二维网格的每个位置开始,如果该位置的字符与目标单词的第一个字符匹配,就开始回溯搜索。在回溯搜索过程中,选择当前位置的上、下、左、右四个相邻位置作为下一步搜索的选择。对于每个选择,首先判断是否满足有效性条件(未越界、未访问过、字符匹配),如果满足则标记为已访问,将字符加入路径并递归搜索下一层。如果搜索到目标单词的所有字符,则返回 True。如果所有选择都搜索完毕仍未找到目标单词,则回溯到上一层,撤销当前选择的访问标记和路径。如果从所有起始位置的搜索都无法找到目标单词,则返回 False。

时间复杂度: O(m*n*4^L)

空间复杂度: O(L)

```python
class Solution:
    def exist(self, board: List[List[str]], word: str) -> bool:
        m = len(board)
        n = len(board[0])

        def backtrack(path, visited, i, j):
            # 如果路径长度等于目标单词长度,说明找到了目标单词,返回 True
            if len(path) == len(word):
                return True
            
            # 获取当前位置的四个相邻位置作为下一步的选择
            choices = [(i-1, j), (i+1, j), (i, j-1), (i, j+1)]

            for choice in choices:
                x, y = choice
                # 判断选择是否有效:未越界、未访问过、字符匹配
                if (x, y) in visited or x < 0 or y < 0 or x >= m or y >= n or word[len(path)] != board[x][y]:
                    continue
                # 标记当前选择为已访问
                visited.add((x, y))
                # 将当前选择的字符加入路径
                path.append(board[x][y])
                # 递归搜索下一层
                if backtrack(path, visited, x, y):
                    return True
                # 回溯,撤销当前选择的路径和访问标记
                path.pop()
                visited.remove((x, y))

        # 遍历二维网格的每个位置作为起点
        for row in range(m):
            for col in range(n):
                # 如果起点字符与目标单词的第一个字符匹配,开始回溯搜索
                if board[row][col] == word[0]:
                    if backtrack([word[0]], {(row, col)}, row, col):
                        return True
        # 如果从所有起点搜索都无法找到目标单词,返回 False
        return False
```

Explore

在此算法中,每次添加字符到路径中前,已经确保该字符与目标单词中对应位置的字符匹配。因此,当路径长度与目标单词长度相等时,可以确信路径中的字符序列与目标单词完全匹配,无需再次检查。

选择的顺序(上、下、左、右)基本上是任意的,主要是为了系统地探索所有可能的相邻位置。改变这个顺序通常不会影响算法的最终结果,但在特定的输入情况下,可能会影响到搜索的效率,例如某个顺序可能稍早找到解决方案,从而减少一些不必要的递归调用。

在此算法中,使用`path`来存储当前路径中的字符,主要用于比较路径长度和目标单词长度,而`visited`用于记录已经访问过的格子,防止在同一路径中重复访问。虽然理论上可以仅用`visited`来跟踪访问状态,但使用`path`可以更直观地管理当前路径的字符,并简化代码逻辑。

在递归函数中,每次递归调用前都会检查当前坐标`(x, y)`是否越界,即检查`x`和`y`是否在合法的范围内(`0 <= x < m` 和 `0 <= y < n`)。这样可以确保所有递归访问都限制在网格的边界内,避免访问非法的内存地址或引发错误。