Leetcode系列-字符串-单词搜索II

单词搜索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;
    }

如果下一个格子的字母不存在,则直接返回;如果最终抵达了某一个单词的最后一个字母,则该单词存在于前缀树中,记录。每到一个格子,标记为#以防重复,向它的四个方向拓展,进入下一层递归。

总结

以上就是我针对本题总结出的两种解题方法,如果有人遇到了此类问题,希望这篇博客会有所帮助。本文有任何不足之处,欢迎随时指正。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值