给定一个 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.lengthn = board[i].length1 <= m, n <= 61 <= word.length <= 15board和word仅由大小写英文字母组成
1. 算法核心思想
本题是一道典型的二维矩阵回溯(Backtracking)题目。其核心在于:以矩阵中的每一个点作为潜在起点,尝试向四个方向“嗅探”目标单词的路径。
解决本题的关键在于“状态保护与还原”:
- 深度优先搜索 (DFS):从一个点出发,只要字符匹配,就继续往深处走。
- 回溯 (Backtracking):当我们走入死胡同(四个方向都走不通)时,必须把当前格子的“已访问”标记清除,退回到上一步。如果不清除标记,当你从另一条路径重新绕回这个格子时,它会被错误地认为“已走过”,导致漏解。
2. 代码逻辑拆解
-
第一阶段:全图扫描寻起点 在
exist函数中,通过双重循环遍历整个m x n的网格。一旦发现字符与word[0]匹配,立即开启func进行深度搜索。只要任意一个起点能走通,整体就返回true。 -
第二阶段:防御性剪枝与匹配判定 在
func入口处进行三重判定:if (ans):如果已经在其他平行宇宙找到了答案,当前分支立刻停止。if (board[x][y] != word[k]):当前格子字符不对,此路不通。if (k == word.size() - 1):能走到这一步且字符匹配,说明整条路径已打通,标记ans = true。
-
第三阶段:标记与扩散
- 标记:进入递归前,将
visited[x][y]置为1。 - 扩散:分别判断上下左右四个方向。只有在坐标合法且未被访问过的情况下,才递进到下一层(
k + 1)。
- 标记:进入递归前,将
-
第四阶段:撤销选择(回溯灵魂) 这是最关键的一行:
visited[x][y] = 0;。当当前的四个方向都探索完毕(不论成功失败),我们必须把这个格子的标记抹去。这保证了在后续的搜索路径中,该格子依然是“可用”的。
class Solution {
public:
bool ans = false;
bool exist(vector<vector<char>>& board, string word) {
int m = board.size();
int n = board[0].size();
vector<vector<int>> visited(m, vector<int>(n, 0));
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
// 对每一个匹配首字母的格子都发起搜索
if (board[i][j] == word[0]) {
func(board, visited, word, 0, i, j);
if (ans) return true;
}
}
}
return false;
}
void func(vector<vector<char>>& board, vector<vector<int>> &visited, string& word, int k, int x, int y) {
if (ans) return;
// 如果当前格子字符不匹配,直接返回
if (board[x][y] != word[k]) return;
// 如果匹配且是最后一个字母
if (k == word.size() - 1) {
ans = true;
return;
}
// 做选择:标记已访问
visited[x][y] = 1;
// 方向数组(上下左右),比写四个 if 更简洁
int dx[4] = {-1, 1, 0, 0};
int dy[4] = {0, 0, -1, 1};
for (int i = 0; i < 4; i++) {
int nextX = x + dx[i];
int nextY = y + dy[i];
// 越界检查和访问检查
if (nextX >= 0 && nextX < board.size() && nextY >= 0 && nextY < board[0].size()) {
if (visited[nextX][nextY] == 0) {
func(board, visited, word, k + 1, nextX, nextY);
}
}
}
// 撤销选择:回溯还原状态(非常重要!)
visited[x][y] = 0;
}
};
3. 复杂度分析
-
时间复杂度:****
其中 是网格中的格子总数, 是字符串
word的长度。对于每一个起点,我们有 3 个方向可以探索(除去来的那个方向)。最坏情况下,我们要从每个格子出发都搜一遍。 -
空间复杂度:****
主要开销来自于
visited标记数组。此外,递归调用的栈深度最大为 。
