单词搜索II理解与分析
题目大意(原题参见leetcode官网)
给定一个单词列表和二维字符网格,找到网格中所有存在于单词列表中的单词并返回。单词与网格中都只包含小写字母。
解题思路
方法一(效率低,会超时)
该方法是我第一次做题时想出的方法,大致思路是:
由于网格的行列数很小,其中的字符均有小写字母构成,所以,第一步,首先遍历网格,将网格中的每种字母及其出现的位置存储到一个哈希表中,哈希表的键为字符char类型,值为pair的数组,即vector<pair<int,int>>。
int m = board.size();
int n = board[0].size();
for(int i = 0; i < m; i ++)
{
for(int j = 0; j < n; j ++)
{
char ch = board[i][j];
mp[ch].push_back(make_pair(i,j));
}
}
第二步,遍历单词列表中的每个单词,通过回溯判断其是否存在于网格中,如果存在,则记录。对于当前的单词,回溯的每一层分析一个字母,利用上一步创建的哈希表,遍历该字母存在的位置,判断其是否与上一个字母的位置相邻,如果相邻,则继续进入下一层分析下一个字母。当扫描完该单词的所有字母后,说明其存在于网格中,则记录下来。最终得到所有存在于网格中的单词。
尝试列表中的每个单词,进入回溯,同时要定义mark数组用于标记走过的网格。
for(int i = 0; i < num_words; i ++)
{
string word = words[i];
found = false;
vector<bool> mark(m*n,false);
backtrack(word,0,start,n,mark);
}
在backtrack函数中,遍历当前字母的的每个位置,如果之前没走过,且相邻于上个字母的格子,则进入下一层。
vector<pair<int,int>> ps = mp[word[index]];
for(int i = 0; i < ps.size() && !found; i ++)
{
pair<int,int> p = ps[i];
int order = (p.first)*n + p.second;
if(pre.first == -1 || (adjoin(pre,p) && !mark[order]))
{
mark[order] = true;
backtrack(word,index + 1,p,n,mark);
mark[order] = false;
}
}
扫描完所有字母则记录。
if(word.length() == index)
{
word_b.push_back(word);
found = true;
return;
}
该方法通过了一部分测试,但在输入数据量较大的情况下,会超出时间限制。本题中单词列表的数量会很大,所以应当减少单词列表遍历时的计算量,方法一的缺点在于没有考虑到这一点,列表遍历的for循环中,每次都要进行backtrack回溯,这是极其耗时的。所以,下面给出前缀树的算法,可以解决这一问题。
方法二 (官网给出的前缀树做法)
前缀树这种数据结构可以高效的存储和检索字符串,特点是在扫描给定字符串的过程中,根据下个字符的类型判断进入哪一个分支,直至扫描完所有字符,就可以实现存储或搜索。其结构定义如下:
class Trie
{
public:
string word;
map<char,Trie*> children;
Trie()
{
this->word = "";
}
void insert(string word)
{
Trie *node = this;
for(char ch: word)
{
if(node->children.count(ch) == 0)
{
node->children[ch] = new Trie();
}
node = node->children[ch];
}
node->word = word;
}
};
该类包含一个成员函数insert,用于向树中插入一个单词,如果单词w存在于这颗树中,并且最后一个字母指向的节点是node,则node节点中的word变量为该单词的字符串w,其他情况下word均为空串。
因此,基于前缀树这种存储结构,解题思路如下:
第一步,将单词列表中的所有单词存储到一棵前缀树中,循环insert就可以了。
Trie* tree = new Trie();
int size = words.size();
for(int i = 0; i < size; i ++)
{
tree->insert(words[i]);
}
第二步,变量棋盘的每一个格子,并从该格出发向周围四个方向拓展,递归调用函数dfs,如果棋盘上该格字符为首字母的单词存在于前缀树中,则记录到set变量res中,最终将res的数据再复制到ans中,返回(这里之所以用res缓存一下,是因为再遍历棋盘的过程中会出现单词重复记录的情况,而set可以自动去重)。
int m = board.size();
int n = board[0].size();
for(int i = 0; i < m; i ++)
{
for(int j = 0; j < n; j ++)
{
dfs(board,i,j,tree);
}
}
for(auto & word: res)
{
ans.emplace_back(word);
}
return ans;
其中,dfs函数为如下:
void dfs(vector<vector<char>>& board,int i,int j,Trie* node)
{
char ch = board[i][j];
if(node->children.count(ch) == 0)
{
return;
}
node = node->children[ch];
if(node->word.size() > 0)
{
res.insert(node->word);
}
board[i][j] = '#';
for(int k = 0; k < 4; k++)
{
int x = i + dir[k][0];
int y = j + dir[k][1];
if(x >= 0 && x < board.size()
&& y >= 0 && y < board[0].size())
{
if(board[x][y] != '#')
dfs(board,x,y,node);
}
}
board[i][j] = ch;
return;
}
如果下一个格子的字母不存在,则直接返回;如果最终抵达了某一个单词的最后一个字母,则该单词存在于前缀树中,记录。每到一个格子,标记为#以防重复,向它的四个方向拓展,进入下一层递归。
总结
以上就是我针对本题总结出的两种解题方法,如果有人遇到了此类问题,希望这篇博客会有所帮助。本文有任何不足之处,欢迎随时指正。