212. Word Search II

原题

Given a 2D board and a list of words from the dictionary, find all words in the board.

Each word must be constructed from letters of sequentially adjacent cell, where “adjacent” cells are those horizontally or vertically neighboring. The same letter cell may not be used more than once in a word.

For example,
Given words = ["oath","pea","eat","rain"] and board =

[
['o','a','a','n'],
['e','t','a','e'],
['i','h','k','r'],
['i','f','l','v']
]

Return ["eat","oath"].

Note:
You may assume that all inputs are consist of lowercase letters a-z.

题意

给定一个二维的字符数组(只有小写字母),找出所有在该board上的单词。每个单词必须在板上连续的,所谓的连续,就是每两个字母水平或竖直相邻,而且board上的每个字母,都不能在一个单词中出现两次。

例如:给定单词words = ["oath","pea","eat","rain"] ,board =
[
['o','a','a','n'],
['e','t','a','e'],
['i','h','k','r'],
['i','f','l','v']
]

代码返回 ["eat","oath"].

解这题之前,可以先做一下另一题79. Word Search,题目如下:

Given a 2D board and a word, find if the word exists in the grid.

The word can be constructed from letters of sequentially adjacent cell, where “adjacent” cells are those horizontally or vertically neighboring. The same letter cell may not be used more than once.

For example,
Given board =

[
[‘A’,’B’,’C’,’E’],
[‘S’,’F’,’C’,’S’],
[‘A’,’D’,’E’,’E’]
]
word = “ABCCED”, -> returns true,
word = “SEE”, -> returns true,
word = “ABCB”, -> returns false.

即实现一个bool判断函数,判断一个单词是否能在board里找到。这题的题意比较明显,即使用DFS来找单词。因为数据量并不大,所以普通的深搜都能通过,我的代码如下,部分解释在代码注释中:

class Solution {
public:
    bool exist(vector<vector<char>>& board, string word) {
        // 一些边界条件的判断
        if(!word.size()) return true;
        int row = board.size();
        if(!row) return false;
        int col = board[0].size();
        if(!col) return false;

        // 开始DFS
        for(int i=0; i<row; i++) {
            for(int j=0; j<col; j++) {
                // 找到了即返回
                if(dfs(board, word, i, j, 0)) {
                    return true;
                }
            }
        }

        // 没有找到
        return false;
    }


    // DFS函数,输入单词board,所找单词,当前指向的行和列,当前所要匹配的单词字母下标
    bool dfs(vector<vector<char>>& board, string word, int row, int col, int pos) {
        if(pos >= word.size()) return true; // 单词已经匹配完了

        // 当前指向的行或列不合法,或者当前board上的字母已经使用(#),或者字母不匹配
        if(row<0 || col<0 || row>=board.size() || col>=board[0].size()
            || board[row][col] == '#' || board[row][col] != word[pos]) return false;

        // 保存当前字符
        char tmp = board[row][col];
        // 标记为已经使用
        board[row][col] = '#';

        // 开始深搜,有其中一个方向找到即算找到
        bool res = dfs(board, word, row-1, col, pos+1)
                    || dfs(board, word, row+1, col, pos+1)
                    || dfs(board, word, row, col-1, pos+1)
                    || dfs(board, word, row, col+1, pos+1);

        // 还原当前字符,回溯
        board[row][col] = tmp;

        return res;
    }
};

逻辑简洁易懂,很快想到可以把它用在212题上,只要把words上的所有单词都遍历检查一遍,就能得到答案了。但是只要略微算一下它的时间复杂度,就知道必定会超时。因此必须想一些办法来加快速度。要么是换一个方法,要么是寻找一个提前剪枝的DFS。

看了下题目中的hint之后,发现可以使用一种叫做Trie,也叫做前缀树的数据结构,提高搜索匹配的速度。因此查看了Trie维基百科的介绍,再把208. Implement Trie (Prefix Tree)数据结构题解决之后,脑中有了新的思路。

首先我介绍一下Trie树,它是一种特殊的树状结构,每个节点装一个字符(根字符为空),子节点数量不定(但是有上限),例如:要把单词[and,as,at,cn,com]构建成一颗Trie树,就会形成成如下图的结构。

Trie树

如果要得到一个单词,就从根节点出发,逐个遍历所需的字母,直到该单词完全匹配,或者下个字母节点为空就退出。
其中的节点结构定义如下:

class TrieNode {
public:
    bool is_word;
    TrieNode* next[26];
    // Initialize your data structure here.
    TrieNode(bool b = false) {
        memset(next, 0, sizeof(next));
        is_word = b;
    }
};

如果一个节点是一个单词的最后一个字母,那么它的is_word属性就为真,表示一个单词的结束。后面可以把is_word属性改成int类型来记录下标或其他信息。

有了Trie树的作用对212题是非常大的,如果把words构造成一棵Trie树,DFS过程中记录搜索记录,然后在Trie树里进行查找,相当于可以同时对words列表里的所有单词进行匹配,节省了非常多的匹配时间!

同样,Trie树的搜索结果也不能仅仅返回bool值,不然如果某个单词是另一个单词的前缀,那么另一个单词就不会被找到了。所以Trie::search()要返回int值,如果是没有找到匹配的前缀就返回-2,找到是前缀但是不是单词,返回-1,找到了单词,返回该单词在words里的下标,用于DFS记录。

为了应对这题,对DFS函数进行了一些改动:

  1. 把范围值改成void,因为不需要获取这个返回值,而用一个vector<int>& retid来记录DFS过程中匹配到的word下标。
  2. 不需要记录当前word匹配到哪个字符了(原pos),而是记录DFS走过的“路径”,用string str表示。
  3. 把坐标合法性判断移到DFS函数的最开始,如果合法就进行str的延伸(即路径延伸),然后再进行Trie树寻找。

为什么要进行第3步修改呢,因为如果不修改的话,以下代码可能会执行三次完全无用的下一步搜索:

        dfs(board, str, trie, row-1, col, retid);
        dfs(board, str, trie, row+1, col, retid);
        dfs(board, str, trie, row, col-1, retid);
        dfs(board, str, trie, row, col+1, retid);

DFS部分过程输出如图:
未修改的DFS
在大数据量的测试样例下会引发超时错误。

如果把左边判断提前之后,DFS的部分代码如下:

void dfs(vector<vector<char>>& board, string str, Trie* trie, int row, int col, set<int>& retid){
        if(row<0 || col<0 || row>=board.size() || col>=board[0].size()
            || board[row][col] == '#') return ;
        str += board[row][col];
        // ......

同样的测试样例DFS的遍历过程如图:
修改后

大大减少了DFS的遍历数量,再次提交也就通过了。

完整代码:

class TrieNode {
public:
    int is_word;
    TrieNode* next[26];
    // Initialize your data structure here.
    TrieNode() {
        memset(next, 0, sizeof(next));
        is_word = -1;
    }
};

class Trie {
public:
    Trie() {
        root = new TrieNode();
    }

    // Inserts a word into the trie.
    void insert(string word, int id) {
        TrieNode* p = root;
        for(int i=0; i<word.size(); i++) {
            if(p->next[word[i] - 'a'] == NULL) {
                p->next[word[i] - 'a'] = new TrieNode();
            }
            p = p->next[word[i] - 'a'];
        }

        p->is_word = id;
    }

    // Returns if the word is in the trie.
    int search(string word) {
        TrieNode *p = find(word);
        if(p==NULL) return -2; // not exist
        else return p->is_word; // is word, return id
    }

    // Returns if there is any word in the trie
    // that starts with the given prefix.
    bool startsWith(string prefix) {
        return find(prefix) != NULL;
    }

private:
    TrieNode* root;
    TrieNode* find(string str) {
        TrieNode* p = root;
        for(int i=0; i<str.size(); i++) {
            if(p->next[str[i]-'a'] == NULL) {
                return NULL;
            }
            p = p->next[str[i]-'a'];
        }

        return p;
    }
};

// Your Trie object will be instantiated and called as such:
// Trie trie;
// trie.insert("somestring");
// trie.search("key");

class Solution {
public:
    vector<string> findWords(vector<vector<char>>& board, vector<string>& words) {
        if(!words.size()) return vector<string>();
        int row = board.size();
        if(!row) return vector<string>();
        int col = board[0].size();
        if(!col) return vector<string>();

        Trie* trie = new Trie;
        for(int i=0; i<words.size(); i++) {
            trie->insert(words[i], i);
        }

        set<int> retid;
        for(int i=0; i<row; i++) {
            for(int j=0; j<col; j++) {
                dfs(board, "", trie, i, j, retid);
            }
        }

        vector<string> result;
        for(auto x : retid) {
            result.push_back(words[x]);
        }

        return result;
    }

    void dfs(vector<vector<char>>& board, string str, Trie* trie, int row, int col, set<int>& retid){
        if(row<0 || col<0 || row>=board.size() || col>=board[0].size()
            || board[row][col] == '#') return ;
        str += board[row][col];
        // cout << str << ", " << row << ", " << col << endl;
        if(str.size() == 0) {
            1^1;
        } if(exist.find(str) == exist.end()) {
            int f = trie->search(str);
            exist[str] = f;
            if(f == -2) return ;
            if(f >= 0) {
                retid.insert(f);
            }
        } else {
            int f = exist[str];
            if(f == -2) return ;
            if(f >= 0) {
                retid.insert(f);
            }
        }

        char tmp = board[row][col];
        board[row][col] = '#';


        dfs(board, str, trie, row-1, col, retid);
        dfs(board, str, trie, row+1, col, retid);
        dfs(board, str, trie, row, col-1, retid);
        dfs(board, str, trie, row, col+1, retid);

        board[row][col] = tmp;

        return;
    }

private:
    map<string, int> exist;
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值