【小白爬Leetcode212】单词搜索II Word SearchII

【小白爬Leetcode212】单词搜索II Word SearchII

  • 题目
    • Discription
    • 分析
  • 思路 Trie树+DFS回溯
    • 改进:

Leetcode212 h a r d \color{#FF0000}{hard} hard
点击进入原题链接:单词搜索II Word SearchII
相关题目:单词搜索 Word Search
【tag】 回溯搜索,字典树

题目

Discription

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.
【小白爬Leetcode212】单词搜索II Word SearchII_第1张图片
Note:

  1. All inputs are consist of lowercase letters a-z.
  2. The values of words are distinct.

中文版描述里多了两个提示:

  • 你需要优化回溯算法以通过更大数据量的测试。你能否早点停止回溯?
  • 如果当前单词不存在于所有单词的前缀中,则可以立即停止回溯。什么样的数据结构可以有效地执行这样的操作?散列表是否可行?为什么? 前缀树如何?

分析

这道题与单词搜索 Word Search 的区别在于这里需要搜索多个单词而不是一个。这带来了两个问题:

  1. 不同word之间的搜索不能相互影响(就是我在上一篇博文所提到的,不能找到了就万事大吉了,还要注意即使在true的情况下也要给board[y][x]解锁)
  2. 即然有多个单词,那么就可能会有大量带有相同前缀的单词,比如app,apple,append这样,如何避免重复搜索呢?

思路 Trie树+DFS回溯

思路如下:

  • 建立一个前缀树Trie,用来储存当前前缀是否能在board中被找到,默认是可以找到,当我们搜索某个单词失败,便记录失败的前缀。例如:搜索apple失败,在搜索到app的时候,就搜索结束了,那么后面遇到诸如app*的单词都不用再搜了。

基于这个朴素的思路,我的代码构成如下,注释写得很详尽了:

class Solution {
public:
	vector<string> findWords(vector<vector<char>>& board, vector<string>& words) {
		if (board.empty() || board[0].empty()) return vector<string>{""};
		vector<string> ans; //结果数组
		int m = board.size();
		int n = board[0].size();
		int dx[] = { 0,1,0,-1 }; //搜索方向
		int dy[] = { 1,0,-1,0 }; //搜索方向
		
		Trie prefix_dic; //创建前缀树,prefix_dic就是根节点
		for (auto word : words) { //遍历words列表里的每一个单词word
			bool flag = true; //当前单词是否含有board里找不到的前缀
			Trie * root = &prefix_dic; //root指针指向Trie树根节点
			
			//遍历Trie树,检查当前单词是否含有board里找不到的前缀
			for (auto ch : word) {
				if (root->child[ch - 'a']) {
					if (!root->child[ch - 'a']->is_prefix_exist) {
						flag = false;
						break; 
					}
					root = root->child[ch - 'a']; //继续遍历Trie树
				}
				else break; //如果没在Trie树里找到说明还没遍历过相同前缀的单词,直接退出遍历
			}
			if (!flag) { //如果有board里找不到的前缀直接结束本次循环,搜索下一个单词
				continue;
			}
			
			bool is_ans = false; //记录当前word是否能在board中找到
			for (int y = 0; y < m; y++) { //开始遍历网格board
				if (is_ans) break;
				for (int x = 0; x < n; x++) {
					if (DFS(&prefix_dic, board, word, 0, x, y, dx, dy, m, n)) { //调用DFS递归函数进行搜索
						ans.push_back(word); //把找到的word加入到结果数组中
						is_ans = true; //并标记当前word的确能被找到
						break;
					}
				}
			}
			//如果搜索失败,需要在Trie树中做记录,后面就不要搜索带这个前缀的单词了
			if(!is_ans) { 
				Trie * root = &prefix_dic;
				for (auto ch : word) { //遍历Trie树找到搜索结束的那个char
					if (root->child[ch - 'a'])  root = root->child[ch - 'a'];
					else {
						root->child[ch - 'a'] = new Trie;
						root->child[ch - 'a']->is_prefix_exist = false;
						break;
					}
				}
			}
		}
		return ans;
	}
	
private:
	struct Trie { //定义Trie树
		Trie* child[26] = { nullptr }; //节点数组
		bool is_prefix_exist = true; //当前前缀是否能在board数组中找到
	};

	bool DFS(Trie* node, vector<vector<char>>& board, const string& word, int word_index, int x, int y, const int* dx, const int* dy, const int& m, const int& n) {
		if (!node->is_prefix_exist) return false; //如果这个前缀不存在于board里,就不要再往下寻找了
		if (word[word_index] != board[y][x]) { // 如果搜寻方向不对直接退出
			return false;
		}
		if (!node->child[word[word_index] - 'a']) {
			node->child[word[word_index] - 'a'] = new Trie;
		}
		node = node->child[word[word_index] - 'a'];

		board[y][x] += 26; //表示当前搜索已访问过board,上锁
		if (word_index == word.size() - 1) {
            board[y][x] -=26; //找到了也别忘记解锁
            return true;
        }
        
		//进行下一个char的搜寻		
		for (int i = 0; i < 4; i++) {
			int new_x = x + dx[i];
			int new_y = y + dy[i];
			if (new_x >= 0 && new_x < n && new_y >= 0 && new_y < m) {
				if (DFS(node, board, word, word_index + 1, new_x, new_y, dx, dy, m, n)) {
					board[y][x] -= 26; //找到了也别忘记解锁
					return true;
				}
			}
		}
		board[y][x] -= 26; //没找到要解锁,回溯
		return false;
	}
};

在这里插入图片描述

改进:

看了官方解答后,发现自己完全想错了。我只用字典树来避免搜索那些已经确定不可能搜到的前缀,但对于那些不确定是否能搜到的单词,却一直在重复遍历(因为按照我之前的思路,只能辨别那些单词一定不会搜到),浪费了大量的机时,几乎和散列表一样复杂,没有发挥Trie树的优势。

这里转换思路,无论当前单词是否能在board中搜索到,每个到叶节点的路径都只遍历一遍。

这里我纠结了一下,是先遍历board矩阵,对每一个board中的元素都从Trie树的根节点开始进行遍历;还是先遍历Trie树,再一个个遍历board的元素。
前者可以通过对Trie树进行剪枝操作来实现每个到叶节点的路径都只遍历一遍,更好实现一点,故选择后者。

class Solution {
public:
	vector<string> findWords(vector<vector<char>>& board, vector<string>& words) {
		if (board.empty() || board[0].empty()) return vector<string>{""};
		vector<string> ans; //结果数组
		int m = board.size();
		int n = board[0].size();
		int dx[] = { 0,1,0,-1 }; //搜索方向
		int dy[] = { 1,0,-1,0 }; //搜索方向
		
        Trie word_dic;
        for(string word : words){
            word_dic.addWord(word);
        }

        for (int y = 0; y < m; y++) { //开始遍历网格board
            for (int x = 0; x < n; x++) {
                    DFS(&word_dic,board,ans,x,y,dx,dy,m,n);
            }
        }
		return ans;
	}
	
private:
	struct Trie { //定义Trie树
		Trie* child[26] = { nullptr }; //节点数组
		string cur_word = "";//当前前缀代表的单词
        void addWord(string word){
            Trie* root = this;
            for(char ch:word){
                if(!root->child[ch-'a']) root->child[ch-'a'] = new Trie;
                root = root->child[ch-'a'];
            }
            root-> cur_word = word;
        }
	};

	void DFS(Trie* parent, vector<vector<char>>& board,vector<string>& ans, int x, int y, const int* dx, const int* dy, const int& m, const int& n) {
        char ch = board[y][x];
        Trie* node = parent->child[ch-'a'];
        if(!node) return; //如果node是空节点,说明当前搜索方向不在Trie树里
        if(node->cur_word!="") {
            ans.push_back(node->cur_word);
            node->cur_word = ""; //防止ans中有重复结果
        }
        
		board[y][x] = '#'; //表示当前搜索已访问过board,上锁
        
		//进行下一个char的搜寻		
		for (int i = 0; i < 4; i++) {
			int new_x = x + dx[i];
			int new_y = y + dy[i];
			if (new_x >= 0 && new_x < n && new_y >= 0 && new_y < m && board[new_y][new_x]!= '#') {
				DFS(node, board, ans, new_x, new_y, dx, dy, m, n);
			}
		}
		board[y][x] = ch; //没找到要解锁,回溯

        //遇到叶节点,剪枝
        bool is_empty = true;
        for(auto it : node->child){
            if(it != nullptr){
                is_empty = false;
                break;
            }
        }
        if(is_empty) parent->child[ch-'a'] = nullptr;
	}
};

性能好多了
在这里插入图片描述

你可能感兴趣的:(小白爬LeetCode,剪枝,dfs,数据结构,回溯,字典树)