79. 单词搜索 (Medium)

专题归类: 08-回溯算法 · 07-图论 LeetCode 链接: https://leetcode.cn/problems/word-search/


在线做题: 写 Python 代码并运行 · Java 跳转 LeetCode

题目描述

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

题目详细分析

数据范围含义:

  • m, n <= 6:网格最多 36 个格子,每个格子作为起点尝试 DFS,总搜索空间可控。
  • word.length <= 15:单词长度不超过 15,DFS 深度可控,但最坏分支为 4^15 约 10 亿,所以剪枝至关重要。
  • 包含大小写字母:board 中的字母区分大小写。

隐藏条件:

  • 同一个单元格不能重复使用,意味着需要 visited 标记(或类似机制)。
  • 只要找到一条路径即可返回 true,不需要找所有路径——这是优化剪枝的关键(找到就提前终止)。
  • 匹配必须严格按顺序,不能跳过或重排字母。

边界条件:

  • word 长度可能大于网格中所有字母的数量,此时直接返回 false。
  • board 只有一个格子且 word 只有一个字母时直接比较。
  • 所有格子都可能作为起点。

小白版直白理解

就像在”找字母”游戏中,给你一张写满字母的格子纸和一个单词,你要从某个格子出发,上下左右移动(不能走对角线),踩过的格子上的字母连起来正好是那个单词。走过的格子不能再走第二次(不然就犯规了)。

比如找”ABCCED”:先找到 A,然后往右找到 B,再往右找到 C,再往下找 C,再往左找 E,再往下找 D——ABCD的顺序连起来就是 ABCCED。如果中间走错路了,就退回一步换方向试。


解题思路

思路一:DFS 回溯 + 原地图标记(推荐)

核心思想: 从每个格子出发,进行深度优先搜索(DFS)。用 # 临时覆盖格子值来标记已访问(省去 visited 数组),回溯时恢复。

搜索路径可视化(以 board=[[“A”,“B”,“C”],[“S”,“F”,“C”],[“A”,“D”,“E”] ], word=“ABC” 为例):

起点 (0,0)=A → 匹配 'A'
  ├→ 右 (0,1)=B → 匹配 'AB'
  │    ├→ 右 (0,2)=C → 匹配 'ABC' ✓ 找到!
  │    ├→ 左 (0,0)=# (已访问) ✗
  │    ├→ 下 (1,1)=F → 'ABF' ✗
  │    └→ 上 越界 ✗
  ├→ 下 (1,0)=S → 'AS' ✗
  ├→ 左 越界 ✗
  └→ 上 越界 ✗
def exist(board, word):
    m, n = len(board), len(board[0])
 
    # 早期剪枝:总字母数不够直接返回 False
    if len(word) > m * n:
        return False
 
    def dfs(i, j, k):
        # 已经匹配完所有字符
        if k == len(word):
            return True
        # 越界或不匹配
        if i < 0 or i >= m or j < 0 or j >= n or board[i][j] != word[k]:
            return False
 
        # 标记已访问(用特殊字符覆盖)
        temp, board[i][j] = board[i][j], '#'
 
        # 四个方向搜索
        found = (dfs(i + 1, j, k + 1) or
                 dfs(i - 1, j, k + 1) or
                 dfs(i, j + 1, k + 1) or
                 dfs(i, j - 1, k + 1))
 
        # 回溯:恢复原字符
        board[i][j] = temp
        return found
 
    # 从每个格子开始尝试
    for i in range(m):
        for j in range(n):
            if dfs(i, j, 0):
                return True
    return False

思路二:DFS + visited 数组

核心思想: 使用一个二维 visited 数组代替原地图标记,思路完全一致,但多 O(mn) 空间。

def exist(board, word):
    m, n = len(board), len(board[0])
    visited = [[False] * n for _ in range(m)]
 
    def dfs(i, j, k):
        if k == len(word):
            return True
        if (i < 0 or i >= m or j < 0 or j >= n or
                visited[i][j] or board[i][j] != word[k]):
            return False
 
        visited[i][j] = True
        found = (dfs(i + 1, j, k + 1) or
                 dfs(i - 1, j, k + 1) or
                 dfs(i, j + 1, k + 1) or
                 dfs(i, j - 1, k + 1))
        visited[i][j] = False
        return found
 
    for i in range(m):
        for j in range(n):
            if dfs(i, j, 0):
                return True
    return False

时间复杂度: O(m x n x 3^L),L 为单词长度(第一个方向确定后剩下 3 个方向,不走回头路)。 空间复杂度: O(L),递归栈深度。


易错点

  1. 忘记回溯还原:标记 visited 后必须在返回前恢复,否则其他起点无法使用该格子。这是网格回溯最常见的问题。
  2. 四方向顺序优化:先搜索最可能的方向(虽然没有通用规则),但短路或 or 天然带剪枝——找到一个就返回。
  3. 索引越界检查顺序:必须先检查越界再访问 board。写成 board[i][j] != word[k] or 越界 会先下标越界报错。
  4. 早期剪枝遗漏:如果 word 长度 > m x n,直接返回 false。这是简单有效的早期剪枝。
  5. Python 字符串不可变性board[i][j] = '#' 直接修改原列表是允许的(列表可变的),但要确保恢复。

框架提炼

网格 DFS 回溯模板:

def dfs(i, j, 状态参数):
    if 达到目标状态:
        return True
    if 越界 or 不匹配 or 已访问:
        return False
 
    标记已访问
    for each 方向 in [上, 下, 左, 右]:
        if dfs(新坐标, 新状态):
            return True  # 找到即返回
    恢复标记
    return False

与标准回溯的区别:

  • 选择不是显式的”遍历选择列表”,而是”尝试四个方向”。
  • visited 标记针对的是网格坐标而非元素索引。
  • 因为只需要找一条路径,短路返回(found 用 or 连接)可大幅减少搜索量。

优化技巧:

  • 反方向剪枝:在上一步方向的反方向上,board 一定是被标记的 visited,实际分支最多 3 个。
  • 词频剪枝:如果 word 中某个字母在 board 中出现次数不足,提前返回 false。

关联题目

  • 200-岛屿数量 — 网格 DFS 的基础题,但不需要回溯(只需要遍历,不需要撤销标记),对比学习”需要回溯 vs 不需要回溯”的场景。
  • 51-N皇后 — 同样是回溯 + 棋盘,N 皇后是逐行放置,单词搜索是逐格匹配。
  • 212-单词搜索II — 本题的进阶版:用 Trie 树优化多个单词的搜索,避免重复遍历。
  • 46-全排列 — 核心回溯思想一致:做选择→递归→撤销选择,只是选择形式不同(排列是选数字,搜索是走方向)。