# [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

board 和 word 仅由大小写英文字母组成

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

这道题的思路与走迷宫问题类似,标准的回溯算法。

即每走一步都有四个方向可选,不断递归,直到匹配到单词为止。

与走迷宫不同的是,该题没有明确的起点与重点,因此需要遍历 board 的所有单元格作为起点。

思路好理解,但问题的难点在于剪枝。完全的回溯复杂度会达到可怕的 O(MN * 3^L),(MN 为 board 长宽,L 为 word.length,每走一步都有 3 个其他方向可选)。在提交时会栈溢出。

那么该如何剪枝呢?除了基本的数组越界,以及 visited 数组访问过的回溯基本剪枝方法外,还有两点是可以做的。

  1. 遍历过程中,发现当前字符与 word 当前位置不匹配,则说明该条子树都不会匹配,直接剪枝。
  2. 当发现有一个深度优先搜索完成了 word 的完全匹配,那么其他所有搜索都不用继续执行了,直接剪枝。

通过这两种方案,可以实现实际执行复杂度上的优化。

function exist(board: string[][], word: string): boolean {
  const m = board.length;
  const n = board[0].length;
  const wordLen = word.length;
  const visited: number[][] = Array(m)
    .fill(0)
    .map(x => Array(n).fill(0));

  for (let i = 0; i < m; i++) {
    for (let j = 0; j < n; j++) {
      const flag = backtrack(i, j, 0);
      // 剪枝:当找到了一次匹配时,不继续进行遍历了
      if (flag) return true;
    }
  }
  return false;

  function backtrack(i: number, j: number, index: number): boolean {
    // 剪枝:数组越界或者被访问过,直接 return
    if (i < 0 || i >= m || j < 0 || j >= n || visited[i][j] !== 0) return false;
    // 剪枝:当前节点与目标不匹配,直接 return
    if (board[i][j] !== word[index]) return false;

    // 已经比对到 word 最后一个字符,且当前节点与目标匹配,则说明找到了答案,直接返回 true。
    if (index === wordLen - 1) {
      return true;
    }

    // 做选择
    visited[i][j] = 1;
    // 递归
    // 剪枝:当发现其中一条路径已经走通,则不进行下面的递归了。
    const result =
      backtrack(i + 1, j, index + 1) ||
      backtrack(i - 1, j, index + 1) ||
      backtrack(i, j + 1, index + 1) ||
      backtrack(i, j - 1, index + 1);
    // 撤销选择
    visited[i][j] = 0;

    return result;
  }
}