# [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 数组访问过的回溯基本剪枝方法外,还有两点是可以做的。
- 遍历过程中,发现当前字符与 word 当前位置不匹配,则说明该条子树都不会匹配,直接剪枝。
- 当发现有一个深度优先搜索完成了 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;
}
}
← [78] 子集 [90] 子集 II →