📌 题目链接:79. 单词搜索 - 力扣(LeetCode)
🔍 难度:中等 | 🏷️ 标签:回溯、深度优先搜索(DFS)、矩阵、字符串
⏱️ 目标时间复杂度:O(MN·3^L) (M,N 为 board 尺寸,L 为 word 长度)
💾 空间复杂度:O(MN) (visited 数组 + 递归栈)
在 LeetCode Hot100 的第一站,我们迎来一道经典的回溯+DFS题目——单词搜索(Word Search) 。这道题不仅是对 DFS 和回溯思想的绝佳训练,也是面试中高频出现的“路径类”问题模板。
无论你是准备校招还是社招,掌握本题所体现的状态标记 + 剪枝 + 路径还原技巧,将为你打开更多二维网格搜索类问题的大门!
🔍 题目分析
给定一个 m x n 的字符矩阵 board 和一个字符串 word,判断该单词是否能由矩阵中相邻(上下左右)且不重复使用的字母按顺序构成。
关键约束:
- 路径不能回头:同一个格子只能用一次。
- 起点任意:可以从任意位置开始匹配。
- 必须连续匹配:字母顺序必须严格对应
word。
📌 典型场景:像“找单词游戏”一样,在字母矩阵中“画出”目标单词。
⚙️ 核心算法及代码讲解
本题的核心是 回溯算法(Backtracking) ,本质是一种带状态恢复的深度优先搜索(DFS) 。
✅ 为什么用回溯?
- 需要尝试所有可能的路径 → 暴力枚举
- 路径有状态(已访问格子)→ 需要标记与撤销
- 一旦某条路径失败,需退回上一步尝试其他方向 → 回溯
🧩 核心函数设计
我们定义一个递归函数:
bool check(board, visited, i, j, word, k)
含义:从 (i, j) 出发,能否匹配 word 从第 k 个字符开始的后缀?
执行逻辑(逐行注释):
// 如果当前格子字符 ≠ word[k],直接失败
if (board[i][j] != s[k]) return false;
// 如果已经匹配到最后一个字符,成功!
else if (k == s.length() - 1) return true;
// 标记当前格子为已访问(防止重复使用)
visited[i][j] = true;
// 定义四个方向:右、左、下、上
vector<pair<int, int>> directions{{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
bool result = false;
for (const auto& dir : directions) {
int newi = i + dir.first, newj = j + dir.second;
// 边界检查 + 是否未访问
if (newi >= 0 && newi < board.size() &&
newj >= 0 && newj < board[0].size() &&
!visited[newi][newj]) {
// 递归尝试下一个字符
bool flag = check(board, visited, newi, newj, s, k + 1);
if (flag) {
result = true;
break; // 找到一条可行路径即可提前退出(剪枝)
}
}
}
// 【关键!】回溯:撤销当前格子的访问标记,供其他路径使用
visited[i][j] = false;
return result;
💡 注意:
visited的“标记-递归-撤销”三步是回溯的灵魂!
🧭 解题思路(分步骤)
-
外层遍历所有起点
- 遍历
board的每个(i, j),只要board[i][j] == word[0],就尝试从此处开始 DFS。
- 遍历
-
内层 DFS + 回溯
- 使用
visited数组记录当前路径已使用的格子。 - 每次进入新格子前检查:是否越界?是否已访问?是否字符匹配?
- 若匹配到最后一个字符,返回
true。 - 否则向四个方向递归,任一方向成功即返回
true。
- 使用
-
剪枝优化
- 一旦某方向找到答案,立即
break,避免无谓搜索。 - 字符不匹配或越界时直接返回,不继续递归。
- 一旦某方向找到答案,立即
-
状态恢复
- 递归返回后,必须将
visited[i][j] = false,否则会影响其他起点的搜索。
- 递归返回后,必须将
📊 算法分析
| 项目 | 分析 |
|---|---|
| 时间复杂度 | 最坏情况:每个起点尝试 3^L 条路径(首步4方向,后续最多3方向),共 MN 个起点 → O(MN·3^L) 。但实际因剪枝远低于此。 |
| 空间复杂度 | visited 数组:O(MN);递归栈深度:O(min(L, MN)) → 总 O(MN) |
| 是否可优化? | ✅ 可加入预剪枝: 1. 统计 word 中各字符频次,若 board 中某字符不足,直接返回 false。 2. 若 word 首尾字符在 board 中出现次数不同,可反转 word 从更少的起点开始(减少无效搜索)。 |
🎯 面试加分点:主动提出上述两种优化,展现工程思维!
💻 代码
✅ C++
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
class Solution {
public:
bool check(vector<vector<char>>& board, vector<vector<int>>& visited, int i, int j, string& s, int k) {
// 当前字符不匹配,直接失败
if (board[i][j] != s[k]) {
return false;
}
// 已匹配到最后一个字符,成功!
else if (k == s.length() - 1) {
return true;
}
// 标记当前位置为已访问
visited[i][j] = true;
// 四个方向:右、左、下、上
vector<pair<int, int>> directions{{0, 1}, {0, -1}, {1, 0}, {-1, 0}};
bool result = false;
for (const auto& dir: directions) {
int newi = i + dir.first, newj = j + dir.second;
// 边界检查 + 未访问检查
if (newi >= 0 && newi < board.size() && newj >= 0 && newj < board[0].size()) {
if (!visited[newi][newj]) {
// 递归匹配下一个字符
bool flag = check(board, visited, newi, newj, s, k + 1);
if (flag) {
result = true;
break; // 找到一条路径即可,剪枝
}
}
}
}
// 【回溯】撤销访问标记,供其他路径使用
visited[i][j] = false;
return result;
}
bool exist(vector<vector<char>>& board, string word) {
int h = board.size(), w = board[0].size();
// 初始化 visited 数组(全0)
vector<vector<int>> visited(h, vector<int>(w));
// 遍历所有可能的起点
for (int i = 0; i < h; i++) {
for (int j = 0; j < w; j++) {
bool flag = check(board, visited, i, j, word, 0);
if (flag) {
return true; // 找到即返回
}
}
}
return false;
}
};
// 测试
signed main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cout.tie(nullptr);
Solution sol;
// 示例1
vector<vector<char>> board1 = {{'A','B','C','E'},{'S','F','C','S'},{'A','D','E','E'}};
cout << sol.exist(board1, "ABCCED") << "\n"; // 输出: 1 (true)
// 示例2
cout << sol.exist(board1, "SEE") << "\n"; // 输出: 1
// 示例3
cout << sol.exist(board1, "ABCB") << "\n"; // 输出: 0 (false)
return 0;
}
✅ JavaScript
var exist = function(board, word) {
const h = board.length, w = board[0].length;
const directions = [[0, 1], [0, -1], [1, 0], [-1, 0]];
const visited = new Array(h);
for (let i = 0; i < visited.length; ++i) {
visited[i] = new Array(w).fill(false);
}
const check = (i, j, s, k) => {
if (board[i][j] != s.charAt(k)) {
return false;
} else if (k == s.length - 1) {
return true;
}
visited[i][j] = true;
let result = false;
for (const [dx, dy] of directions) {
let newi = i + dx, newj = j + dy;
if (newi >= 0 && newi < h && newj >= 0 && newj < w) {
if (!visited[newi][newj]) {
const flag = check(newi, newj, s, k + 1);
if (flag) {
result = true;
break;
}
}
}
}
visited[i][j] = false; // 回溯
return result;
};
for (let i = 0; i < h; i++) {
for (let j = 0; j < w; j++) {
const flag = check(i, j, word, 0);
if (flag) {
return true;
}
}
}
return false;
};
🌟 结语
🌟 本期完结,下期见!🔥
👉 点赞收藏加关注,新文更新不迷路。关注专栏【算法】LeetCode Hot100刷题日记,持续为你拆解每一道热题的底层逻辑与面试技巧!
💬 欢迎留言交流你的解法或疑问!一起进步,冲向 Offer!💪
📌 记住:当你在刷题时,不要只看答案,要像写这篇文章一样,深入思考每一步背后的原理、优化空间和面试价值。这才是真正提升算法能力的方式!