1. 题目解析与核心思路
这道单词搜索题是LeetCode上经典的二维网格搜索问题,属于中等难度。题目要求在一个m×n的字符网格中查找是否存在某个单词,单词的字母必须按照顺序通过相邻(上下左右)的单元格连接,且每个单元格只能使用一次。
1.1 问题特征分析
这类二维网格搜索问题通常具有以下特点:
- 有限搜索空间:网格尺寸最大为6×6,单词长度不超过15
- 路径依赖:后续字母的选择依赖于前一个字母的位置
- 状态回溯:需要记录已访问的单元格,并在搜索失败时恢复状态
1.2 解题思路选择
面对这类问题,我们通常会考虑以下几种算法:
- 深度优先搜索(DFS):适合路径探索,天然支持回溯
- 广度优先搜索(BFS):需要额外记录路径信息,实现较复杂
- 动态规划:难以处理路径不重复的限制
DFS因其简洁的实现和自然的回溯机制成为最优选择。具体来说,我们需要:
- 遍历网格找到单词首字母
- 从该位置开始DFS搜索后续字母
- 使用回溯法管理访问状态
2. 深度优先搜索实现详解
2.1 基础DFS框架
DFS算法的核心是递归探索所有可能路径。对于本问题,我们需要:
- 终止条件:当匹配完所有字母时返回成功
- 递归过程:对当前字母的四个相邻位置进行探索
- 剪枝条件:越界检查、字母匹配检查、重复访问检查
2.2 关键实现细节
2.2.1 方向数组技巧
使用dx/dy数组表示四个方向的变化量:
cpp复制int dx[4] = {0, 0, -1, 1}; // 左右上下
int dy[4] = {1, -1, 0, 0};
这种写法比分别处理四个方向更简洁,也更容易扩展。
2.2.2 回溯标记方法
在DFS过程中需要标记已访问的单元格,常见方法有:
- 使用额外visited数组(空间O(mn))
- 原地修改网格值(空间O(1))
示例代码采用第二种方法,通过临时修改网格值为0来标记访问:
cpp复制char c = board[xx][yy];
board[xx][yy] = 0; // 标记访问
bool flag = dfs(xx, yy, po + 1, s, board, n, m);
board[xx][yy] = c; // 恢复原值
2.3 完整代码解析
cpp复制class Solution {
public:
int dx[4] = {0, 0, -1, 1}, dy[4] = {1, -1, 0, 0};
bool exist(vector<vector<char>>& board, string word) {
int n = board.size();
int m = board[0].size();
for(int i = 0; i < n; ++i) {
for(int j = 0; j < m; ++j) {
if(board[i][j] == word[0]) {
char c = board[i][j];
board[i][j] = 0;
bool flag = dfs(i, j, 1, word, board, n, m);
board[i][j] = c;
if(flag) return true;
}
}
}
return false;
}
bool dfs(int x, int y, int po, string &s, vector<vector<char>>& board, int n, int m) {
if(po == s.size()) return true;
for(int i = 0; i < 4; ++i) {
int xx = x + dx[i], yy = y + dy[i];
if(xx >=0 && yy >= 0 && xx < n && yy < m && board[xx][yy] == s[po]) {
char c = board[xx][yy];
board[xx][yy] = 0;
bool flag = dfs(xx, yy, po + 1, s, board, n, m);
board[xx][yy] = c;
if(flag) return true;
}
}
return false;
}
};
3. 算法优化与性能分析
3.1 时间复杂度分析
最坏情况下,算法需要:
- 遍历所有起始点:O(mn)
- 每个起始点进行DFS:O(4^L),L为单词长度
因此总时间复杂度为O(mn × 4^L)
对于题目限制(mn≤36, L≤15),最坏情况下4^15≈1亿次操作,但实际由于剪枝,性能可以接受。
3.2 空间复杂度分析
主要空间消耗来自:
- 递归栈深度:O(L)
- 原地修改网格:O(1)额外空间
总空间复杂度为O(L)
3.3 优化方向
- 预处理检查:先统计网格和单词的字符频率,快速排除不可能情况
- 双向搜索:同时从单词首尾开始搜索
- Trie树优化:对于多单词搜索场景
4. 常见问题与调试技巧
4.1 典型错误案例
- 忘记恢复网格状态:导致后续搜索失败
- 越界检查不完整:数组访问越界
- 终止条件错误:在po == s.size()-1时返回
4.2 调试建议
- 打印搜索路径:输出当前匹配的字母和位置
- 可视化网格状态:标记已访问的单元格
- 小规模测试:使用2×2网格验证边界条件
4.3 测试用例设计
有效测试用例应包含:
- 常规情况:示例中的ABCCED、SEE
- 边界情况:单字母单词、网格全相同字母
- 失败情况:不存在的单词、字母不足
- 性能测试:最大尺寸网格和长单词
5. 扩展与变种问题
5.1 多单词搜索
如果需要搜索多个单词,可以:
- 对每个单词单独调用exist函数
- 使用Trie树优化批量搜索
5.2 允许重复使用单元格
若允许重复使用单元格,只需移除回溯标记部分,但需要注意避免无限循环。
5.3 找出所有路径
如果需要返回所有有效路径,可以:
- 将返回值改为vector<vector<pair<int,int>>>
- 在找到匹配时记录当前路径
6. 实际应用场景
这类算法在以下场景有实际应用:
- 单词游戏实现(如Boggle)
- 生物信息学中的序列匹配
- 路径规划中的障碍物规避
- 图像处理中的连通区域分析
在准备算法面试时,建议不仅掌握解法,还要理解其应用场景和变种,这样才能在面试中游刃有余。我个人的经验是,这类DFS+回溯的问题需要至少亲手实现3-5次,才能对各种边界条件处理形成肌肉记忆。