1. 题目解析与核心需求
"矩阵中的路径"是《剑指Offer》第65题,考察的是回溯算法在二维矩阵中的应用。题目要求在一个字符矩阵中判断是否存在一条路径,该路径上的字符按顺序连接起来恰好能组成给定的目标字符串。这类问题在图像处理、机器人路径规划、文字识别等领域有广泛应用。
从工程角度看,这道题考察了几个关键能力:
- 二维矩阵的遍历技巧
- 回溯算法的实现与剪枝优化
- 边界条件的处理能力
- 递归与迭代的转换思维
注意:实际面试中,面试官可能会要求同时给出递归和迭代两种解法,并分析各自的时空复杂度差异。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 问题建模与算法选择
2.1 问题形式化定义
给定一个m×n的字符矩阵board和一个字符串word,需要判断矩阵中是否存在一条由相邻单元格(上下左右)连接而成的路径,其字符顺序与word完全匹配。相邻单元格不能重复使用。
示例:
code复制board = [
['A','B','C','E'],
['S','F','C','S'],
['A','D','E','E']
]
word = "ABCCED" → 返回true
2.2 算法选择依据
回溯算法是解决此类问题的首选,因为:
- 需要尝试多种可能的路径组合
- 存在明确的失败条件可以提前终止搜索
- 每一步的选择空间有限(最多4个方向)
- 需要记录已访问的节点防止重复使用
与DFS的区别:
- 回溯会在发现当前路径不可能时立即返回(剪枝)
- 需要维护访问状态矩阵
- 找到解后可以立即终止搜索
3. 递归实现详解
3.1 基础递归框架
python复制def exist(board, word):
def dfs(i, j, k):
if not 0 <= i < len(board) or not 0 <= j < len(board[0]) or board[i][j] != word[k]:
return False
if k == len(word) - 1:
return True
tmp, board[i][j] = board[i][j], '/'
res = 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] = tmp
return res
for i in range(len(board)):
for j in range(len(board[0])):
if dfs(i, j, 0):
return True
return False
3.2 关键实现细节
- 访问标记技巧:通过临时修改board[i][j]为特殊字符'/'实现标记,比维护单独的visited矩阵更节省空间
- 递归终止条件:
- 越界检查(i,j是否合法)
- 字符不匹配检查
- 成功匹配检查(k达到word长度)
- 方向遍历顺序:代码中按"下上右左"顺序尝试,实际测试表明调整顺序可能影响运行效率
3.3 复杂度分析
时间复杂度:
- 最坏情况O(MN×3^L),其中MN是矩阵大小,L是word长度
- 3^L是因为每次递归有3个新方向可选(不能回头)
空间复杂度:
- 递归栈深度O(L)
- 修改原矩阵避免了O(MN)的额外空间
4. 迭代解法实现
4.1 使用栈模拟递归
python复制def exist_iterative(board, word):
m, n = len(board), len(board[0])
directions = [(0,1),(1,0),(0,-1),(-1,0)]
for i in range(m):
for j in range(n):
if board[i][j] != word[0]:
continue
stack = [(i, j, 0, False)]
while stack:
x, y, idx, visited = stack.pop()
if visited:
board[x][y] = word[idx]
continue
if idx == len(word) - 1:
return True
if board[x][y] != word[idx]:
continue
board[x][y] = '#'
stack.append((x, y, idx, True))
for dx, dy in directions:
nx, ny = x + dx, y + dy
if 0 <= nx < m and 0 <= ny < n:
stack.append((nx, ny, idx + 1, False))
return False
4.2 迭代法特点
- 双状态标记:使用(visited=False)表示待处理节点,(visited=True)表示回溯阶段
- 显式栈结构:栈元素保存(x,y,idx,visited)四元组
- 方向处理顺序:与递归不同,迭代法通常逆序处理方向(因为栈的LIFO特性)
5. 性能优化策略
5.1 预处理剪枝
python复制# 在正式搜索前添加这些检查
from collections import defaultdict
def exist_optimized(board, word):
# 检查word字符是否都在board中
board_chars = defaultdict(int)
for row in board:
for c in row:
board_chars[c] += 1
word_chars = defaultdict(int)
for c in word:
word_chars[c] += 1
for c in word_chars:
if word_chars[c] > board_chars.get(c, 0):
return False
# 其余代码与之前相同...
5.2 搜索起点优化
统计word首尾字符出现频率,从较少的一方开始搜索:
python复制start_char = word[0]
end_char = word[-1]
if sum(row.count(start_char) for row in board) > sum(row.count(end_char) for row in board):
word = word[::-1]
5.3 并行搜索
对于大规模矩阵,可以分割区域并行搜索:
python复制from concurrent.futures import ThreadPoolExecutor
def parallel_search(board, word):
m = len(board)
with ThreadPoolExecutor() as executor:
futures = []
for i in range(m):
futures.append(executor.submit(search_row, board, word, i))
for future in futures:
if future.result():
return True
return False
6. 边界条件与测试用例
6.1 必须考虑的边界情况
- 空矩阵或空word
- word长度超过矩阵元素总数
- 矩阵中存在重复字符
- 路径需要"折返"的情况
- 超大矩阵性能测试
6.2 典型测试用例
python复制test_cases = [
# 常规情况
([["A","B","C"],["D","E","F"]], "ABED", True),
# 需要折返
([["A","B","C"],["D","E","F"]], "ABEF", False),
# 重复字符
([["A","A","A"],["A","A","A"]], "AAAAAA", True),
# 单字符
([["A"]], "A", True),
# 空word
([["A","B"],["C","D"]], "", True),
# 不存在的路径
([["A","B"],["C","D"]], "ACB", False)
]
7. 实际工程应用场景
7.1 文字识别中的单词查找
在OCR后处理中,需要从字符矩阵中识别可能的单词:
python复制def find_words_in_puzzle(puzzle, dictionary):
found = set()
for word in dictionary:
if exist(puzzle, word):
found.add(word)
return found
7.2 机器人路径规划
将环境建模为矩阵,寻找可行路径:
python复制def find_robot_path(grid, start, end):
# 将grid中的障碍物标记为'#'
# 使用修改后的exist算法寻找start到end的路径
pass
7.3 基因序列匹配
在生物信息学中,DNA序列可以表示为字符矩阵:
python复制def find_gene_pattern(dna_matrix, pattern):
# dna_matrix是2D的ACGT序列
return exist(dna_matrix, pattern)
8. 常见错误与调试技巧
8.1 典型错误模式
- 忘记恢复矩阵状态:导致后续搜索出错
- 方向遍历顺序不当:可能引发栈溢出
- 边界检查不完整:数组越界错误
- 递归终止条件错误:漏掉k=len(word)-1的情况
8.2 调试方法
- 可视化追踪:打印当前搜索路径
python复制def dfs(i, j, k, path):
print(f"At ({i},{j}) matching '{word[k]}', path: {path}")
# ...其余代码...
-
小矩阵测试:用2x2或3x3矩阵验证
-
性能分析:使用cProfile模块
python复制import cProfile
cProfile.run('exist(large_board, long_word)')
9. 变种问题拓展
9.1 允许重复访问单元格
修改条件允许重复使用单元格:
python复制# 只需移除标记/恢复的代码
res = 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] = tmp
9.2 查找所有可能路径
收集所有有效路径而非仅判断存在:
python复制def find_all_paths(board, word):
res = []
def dfs(i, j, k, path):
# ...相同条件检查...
if k == len(word) - 1:
res.append(path + [(i,j)])
return
# ...递归调用...
# ...遍历起始点...
return res
9.3 三维矩阵路径
扩展到三维空间(x,y,z):
python复制directions = [(1,0,0),(-1,0,0),(0,1,0),(0,-1,0),(0,0,1),(0,0,-1)]
# 递归时需要增加z维度处理
10. 不同语言实现对比
10.1 Java实现特点
java复制class Solution {
public boolean exist(char[][] board, String word) {
for (int i = 0; i < board.length; i++) {
for (int j = 0; j < board[0].length; j++) {
if (dfs(board, word, i, j, 0)) {
return true;
}
}
}
return false;
}
private boolean dfs(char[][] board, String word, int i, int j, int k) {
if (k == word.length()) return true;
if (i < 0 || i >= board.length || j < 0 || j >= board[0].length
|| board[i][j] != word.charAt(k)) {
return false;
}
char tmp = board[i][j];
board[i][j] = '#';
boolean res = dfs(board, word, i+1, j, k+1)
|| dfs(board, word, i-1, j, k+1)
|| dfs(board, word, i, j+1, k+1)
|| dfs(board, word, i, j-1, k+1);
board[i][j] = tmp;
return res;
}
}
10.2 C++实现优化
cpp复制class Solution {
public:
bool exist(vector<vector<char>>& board, string word) {
for (int i = 0; i < board.size(); ++i) {
for (int j = 0; j < board[0].size(); ++j) {
if (dfs(board, word, i, j, 0)) {
return true;
}
}
}
return false;
}
bool dfs(vector<vector<char>>& board, string& word, int i, int j, int k) {
if (k == word.size()) return true;
if (i < 0 || i >= board.size() || j < 0 || j >= board[0].size()
|| board[i][j] != word[k]) {
return false;
}
board[i][j] = '\0';
bool res = dfs(board, word, i+1, j, k+1)
|| dfs(board, word, i-1, j, k+1)
|| dfs(board, word, i, j+1, k+1)
|| dfs(board, word, i, j-1, k+1);
board[i][j] = word[k];
return res;
}
};
10.3 JavaScript实现注意点
javascript复制function exist(board, word) {
const dfs = (i, j, k) => {
if (k === word.length) return true;
if (i < 0 || i >= board.length || j < 0 || j >= board[0].length
|| board[i][j] !== word[k]) {
return false;
}
const tmp = board[i][j];
board[i][j] = '';
const res = dfs(i+1,j,k+1) || dfs(i-1,j,k+1)
|| dfs(i,j+1,k+1) || dfs(i,j-1,k+1);
board[i][j] = tmp;
return res;
};
for (let i = 0; i < board.length; i++) {
for (let j = 0; j < board[0].length; j++) {
if (dfs(i, j, 0)) return true;
}
}
return false;
}
11. 面试考察要点
11.1 面试官期望看到的
- 清晰的解题思路:能够明确说明选择回溯算法的理由
- 完整的边界处理:对各种异常情况的考虑
- 代码规范性:良好的变量命名和代码结构
- 复杂度分析能力:准确计算时间和空间复杂度
- 优化意识:能主动提出可能的优化方案
11.2 常见follow-up问题
- 如何改为查找所有可能的路径?
- 如果矩阵很大但word很短,如何优化?
- 如何改为并行搜索?
- 如果允许斜向移动(8个方向),如何修改代码?
- 如何测试这个算法的正确性?
12. 学习路径建议
12.1 相关题目推荐
- LeetCode 79:单词搜索(本题)
- LeetCode 212:单词搜索 II(Trie树+回溯)
- LeetCode 980:不同路径 III(带有障碍的网格)
- LeetCode 1219:黄金矿工(带权路径)
- LeetCode 63:不同路径 II(动态规划解法)
12.2 延伸学习资料
- 《算法导论》回溯算法章节
- 《剑指Offer》相关题目解析
- 回溯算法可视化工具:Visualgo.net
- MIT 6.006 Introduction to Algorithms课程
- 《编程珠玑》中的算法优化案例
在实际开发中遇到类似矩阵路径问题时,我通常会先画出小规模案例的搜索树,明确状态转移规则后再编码。调试时使用颜色标记已访问单元格的方法特别有效——在IDE中可以用不同背景色显示矩阵状态变化,这样能直观发现回溯是否正确恢复了状态。
