Hard
题目描述
给定一个 m x n 二维字符网格 board 和一个单词数组 words,找出所有同时在二维网格和字典中出现的单词。
单词必须按照字母顺序,通过相邻的单元格内的字母构成,其中"相邻"单元格是那些水平相邻或垂直相邻的单元格。同一个单元格内的字母在一个单词中不允许被重复使用。
示例 1:
输入:board = [["o","a","a","n"],["e","t","a","e"],["i","h","k","r"],["i","f","l","v"]], words = ["oath","pea","eat","rain"]
输出:["eat","oath"]
示例 2:
输入:board = [["a","b"],["c","d"]], words = ["abcb"]
输出:[]
提示:
m == board.lengthn == board[i].length1 <= m, n <= 12board[i][j]是小写英文字母1 <= words.length <= 3 * 10^41 <= words[i].length <= 10words[i]由小写英文字母组成words中的所有字符串互不相同
解题思路
这是一道经典的字典树(Trie)+ DFS 回溯问题。如果对每个单词单独进行 DFS 搜索,时间复杂度会很高。
核心思路:
- 构建字典树(Trie):将所有待查找的单词插入 Trie 中,这样可以在 DFS 过程中快速判断当前路径是否可能构成有效单词
- DFS + 回溯:从网格的每个位置开始进行深度优先搜索,利用 Trie 进行剪枝优化
- 剪枝策略:当当前路径不是任何单词的前缀时,立即停止搜索
优化技巧:
- 使用 Trie 的
isWord标记来识别完整单词 - 在找到单词后将
isWord设为false,避免重复添加 - 当 Trie 节点没有子节点且不是单词结尾时,可以删除该节点进行进一步优化
- 使用 visited 数组标记已访问的位置,防止重复使用同一单元格
时间复杂度: O(M×N×4^L),其中 M、N 是网格尺寸,L 是单词的最大长度。实际运行中由于 Trie 的剪枝效果,性能会显著提升。
代码实现
class Solution {
public:
struct TrieNode {
TrieNode* children[26];
string word;
TrieNode() {
for (int i = 0; i < 26; i++) {
children[i] = nullptr;
}
word = "";
}
};
void buildTrie(TrieNode* root, vector<string>& words) {
for (string& word : words) {
TrieNode* node = root;
for (char c : word) {
int idx = c - 'a';
if (!node->children[idx]) {
node->children[idx] = new TrieNode();
}
node = node->children[idx];
}
node->word = word;
}
}
void dfs(vector<vector<char>>& board, int i, int j, TrieNode* node, vector<string>& result) {
char c = board[i][j];
if (c == '#' || !node->children[c - 'a']) return;
node = node->children[c - 'a'];
if (!node->word.empty()) {
result.push_back(node->word);
node->word = ""; // 避免重复
}
board[i][j] = '#'; // 标记为已访问
int directions[4][2] = {{-1,0}, {1,0}, {0,-1}, {0,1}};
for (auto& dir : directions) {
int ni = i + dir[0], nj = j + dir[1];
if (ni >= 0 && ni < board.size() && nj >= 0 && nj < board[0].size()) {
dfs(board, ni, nj, node, result);
}
}
board[i][j] = c; // 回溯
}
vector<string> findWords(vector<vector<char>>& board, vector<string>& words) {
TrieNode* root = new TrieNode();
buildTrie(root, words);
vector<string> result;
int m = board.size(), n = board[0].size();
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
dfs(board, i, j, root, result);
}
}
return result;
}
};
class Solution:
def findWords(self, board: List[List[str]], words: List[str]) -> List[str]:
class TrieNode:
def __init__(self):
self.children = {}
self.word = ""
def build_trie(root, words):
for word in words:
node = root
for char in word:
if char not in node.children:
node.children[char] = TrieNode()
node = node.children[char]
node.word = word
def dfs(i, j, node):
char = board[i][j]
if char not in node.children:
return
node = node.children[char]
if node.word:
result.append(node.word)
node.word = "" # 避免重复
board[i][j] = '#' # 标记为已访问
directions = [(-1, 0), (1, 0), (0, -1), (0, 1)]
for di, dj in directions:
ni, nj = i + di, j + dj
if 0 <= ni < m and 0 <= nj < n and board[ni][nj] != '#':
dfs(ni, nj, node)
board[i][j] = char # 回溯
root = TrieNode()
build_trie(root, words)
result = []
m, n = len(board), len(board[0])
for i in range(m):
for j in range(n):
dfs(i, j, root)
return result
public class Solution {
public class TrieNode {
public TrieNode[] Children = new TrieNode[26];
public string Word = "";
}
private void BuildTrie(TrieNode root, string[] words) {
foreach (string word in words) {
TrieNode node = root;
foreach (char c in word) {
int idx = c - 'a';
if (node.Children[idx] == null) {
node.Children[idx] = new TrieNode();
}
node = node.Children[idx];
}
node.Word = word;
}
}
private void DFS(char[][] board, int i, int j, TrieNode node, IList<string> result) {
char c = board[i][j];
if (c == '#' || node.Children[c - 'a'] == null) return;
node = node.Children[c - 'a'];
if (!string.IsNullOrEmpty(node.Word)) {
result.Add(node.Word);
node.Word = ""; // 避免重复
}
board[i][j] = '#'; // 标记为已访问
int[,] directions = {{-1,0}, {1,0}, {0,-1}, {0,1}};
for (int d = 0; d < 4; d++) {
int ni = i + directions[d, 0];
int nj = j + directions[d, 1];
if (ni >= 0 && ni < board.Length && nj >= 0 && nj < board[0].Length) {
DFS(board, ni, nj, node, result);
}
}
board[i][j] = c; // 回溯
}
public IList<string> FindWords(char[][] board, string[] words) {
TrieNode root = new TrieNode();
BuildTrie(root, words);
IList<string> result = new List<string>();
int m = board.Length, n = board[0].Length;
for (int i = 0; i < m; i++) {
for (int j = 0; j < n; j++) {
DFS(board, i, j, root, result);
}
}
return result;
}
}
var findWords = function(board, words) {
class TrieNode {
constructor() {
this.children = {};
this.word = "";
}
}
function buildTrie(root, words) {
for (let word of words) {
let node = root;
for (let char of word) {
if (!(char in node.children)) {
node.children[char] = new TrieNode();
}
node = node.children[char];
}
node.word = word;
}
}
function dfs(i, j, node) {
let char = board[i][j];
if (!(char in node.children)) return;
node = node.children[char];
if (node.word) {
result.push(node.word);
node.word = ""; // 避免重复
}
board[i][j] = '#'; // 标记为已访问
let directions = [[-1, 0], [1, 0], [0, -1], [0, 1]];
for (let [di, dj] of directions) {
let ni = i + di, nj = j + dj;
if (ni >= 0 && ni < m && nj >= 0 && nj < n && board[ni][nj] !== '#') {
dfs(ni, nj, node);
}
}
board[i][j] = char; // 回溯
}
let root = new TrieNode();
buildTrie(root, words);
let result = [];
let m = board.length, n = board[0].length;
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
dfs(i, j, root);
}
}
return result;
};
复杂度分析
| 复杂度类型 | 分析 |
|---|---|
| 时间复杂度 | O(M×N×4^L),其中 M、N 是网格尺寸,L 是单词最大长度。实际运行中由于 Trie 剪枝优化,性能会显著提升 |
| 空间复杂度 | O(K×L),其中 K 是单词数量,L 是单词平均长度,主要用于存储 Trie 树 |
相关题目
. Word Search (Medium)
. Unique Paths III (Hard)