例题:(1)Acwing 835. Trie字符串统计
Trie树是一个可以高效存储查询字符串的数据结构。将一个字符串的每一个字符作为一个根节点,从字符串头到字符串尾连接起来。因此我们可以把每一个字符串存储为一个节点,记录其子节点的位置。存储时对每一个字符串尾进行标记,查询时从根节点开始遍历。
#include <iostream>
#include <cstring>
#include <algorithm>
#include <string>
using namespace std;
const int N = 1e5+10,M = 26;
int son[N][M],idx,cnt[N];
void insert(string str){int len = str.length(),p = 0;for(int i = 0;i<len;i++){int t = str[i] - 'a';if(!son[p][t]) son[p][t] = ++idx;p = son[p][t];}cnt[p]++;
}
int query(string str){int len = str.length(),p = 0;for(int i = 0;i<len;i++){int t = str[i] - 'a';if(!son[p][t]) return 0;p = son[p][t];}return cnt[p];
}
int main()
{int n;scanf("%d", &n);while(n--){char op[2];scanf("%s", op);string str;cin >> str;if(op[0] == 'I'){insert(str);}else{printf("%d\n",query(str));}}return 0;
}
(2) AcWing 143. 最大异或对
暴力做法肯定会超时,因此我们对xor运算进行考虑。
每个数可以被看做31位的二进制数,而xor运算的最大值意味着从最高位开始尽量不同。因此可以用trie树进行存储,存储每一个数并对其与前面已存在的trie树进行xor运算(查看每一位所在的节点是否有与其不同的儿子存在,如果有则*2+1,否则*2)
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 31e5+10;
int idx,son[N][2];
void insert(int n){int p = 0;for(int i = 30;i>=0;i--){int t = n >> i & 1;if(!son[p][t]) son[p][t] = ++idx;p = son[p][t];}
}
int query(int x){int res = 0,p = 0;for(int i = 30;i>=0;i--){int t = x >> i & 1;if(son[p][!t]){p = son[p][!t];res = res*2 + 1;}else{p = son[p][t];res *= 2;}}return res;
}
int main()
{int n;scanf("%d",&n);int res = 0;for(int i = 0;i<n;i++){int x;scanf("%d", &x);insert(x);res = max(res,query(x));}printf("%d",res);return 0;
}
练习:(1)Leetcode 211 添加与搜索单词
。。生病因此状态不佳。。
这个Trie树的查找相比一般的Trie树更为麻烦,因为他的搜索字符串中含有万能字符‘.’,所以在匹配时需要分情况讨论,一般字符正常匹配即可,而字符为‘.’时遍历判断其对应的节点是否有子节点即可。因此需要用爆搜来实现。
class WordDictionary {
public:int idx = 0,son[250010][26] = {0};bool vis[250010] = {0};WordDictionary() {}void addWord(string word) {int len = word.length(),p = 0;for(int i = 0;i<len;i++){int u = word[i] - 'a';if(!son[p][u]) son[p][u] = ++idx;p = son[p][u];}vis[p] = true;}bool search(string word) {return dfs(word,0,0);}bool dfs(string word,int index,int root){if(index == word.length()) return vis[root];bool f = false;if(word[index] == '.'){for(int i = 0;i<26;i++){if(son[root][i] == 0) continue;f = dfs(word,index+1,son[root][i]);if(f) break;}}else{if(son[root][word[index] - 'a']){f = dfs(word,index + 1,son[root][word[index]-'a']);}}return f;}
};
(2)Acwing 161.电话列表
题目没什么难的。。贴上来是为了让大家注意坑点。。输入不能随意跳出循环否则剩下没输入的会顺延输入导致全部错乱。。。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 600010,M = 10;
int son[N][M],idx;
bool cnt[N];
bool query(string x){int len = x.length(),p = 0;for(int i = 0;i<len;i++){int u = x[i] - '0';if(!son[p][u]) return false;p = son[p][u];if(cnt[p]) return true;}return true;
}
void insert(string x){int len = x.length(),p = 0;for(int i = 0;i<len;i++){int u = x[i] - '0';if(!son[p][u]) son[p][u] = ++idx;p = son[p][u];}cnt[p] = true;
}
void init(){idx = 0;memset(son[0],0,sizeof(son));memset(cnt,0,sizeof(cnt));
}
int main()
{int t;scanf("%d", &t);while(t--){init();int n;bool flag = 0;scanf("%d", &n);for(int i = 0;i<n;i++){string str;cin >> str;if(flag) continue;if(!query(str)) insert(str);else{printf("NO\n");flag = 1;}}if(!flag) printf("YES\n");}return 0;
}
(3)Acwing 142. 前缀统计
对父子节点的关系还是不太清楚。。这题其实没啥难度,主要是何时求取节点标记需要好好思考。一定是先更新到子节点,再对标记值累加。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e6+10,M = 26;
int son[N][M],idx,cnt[N];
void insert(string str){int p = 0,len = str.length();for(int i = 0;i<len;i++){int u = str[i] - 'a';if(!son[p][u]) son[p][u] = ++idx;p = son[p][u];}cnt[p]++;
}
int query(string str){int p = 0,len = str.length(),res = 0;for(int i = 0;i<len;i++){int u = str[i] - 'a';if(!son[p][u]) break;p = son[p][u];res += cnt[p];}return res;
}
int main()
{int n,m;scanf("%d%d", &n, &m);for(int i = 0;i<n;i++){string str;cin >> str;insert(str);}for(int i = 0;i<m;i++){string str;cin >> str;printf("%d\n",query(str));}return 0;
}
(4)Acwing 5304. 最高频字符串
讲道理其实还是没啥难度的题。。还是想记录个小细节。。
当一个函数同时存在返回值和其他附加功能的时候(比如并查集的find函数存在路径加速和返回根节点两个作用),一定要注意不要重复调用,否则可能会出现重复累加(带权并查集也是如此。。)这题的insert函数就是很好的例子。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010,M = 26;
int son[N][M],idx,cnt[N];
int insert(string str){int p = 0,len = str.length();for(int i = 0;i<len;i++){int u = str[i] - 'a';if(!son[p][u]) son[p][u] = ++idx;p = son[p][u];}cnt[p]++;return cnt[p];
}
int main()
{int n;scanf("%d", &n);int res = -1;string ans = "";for(int i = 0;i<n;i++){string c;cin >> c;int k = insert(c);if(k > res || (k == res && c < ans) ){ans = c;res = k;}}cout << ans;return 0;
}
(5) Leetcode 1065.字符串的索引对
我还以为有什么好方法。。原来也是无脑暴力。。
把word里的字符串存入Trie树,然后枚举text中的每一个子串去Trie树中查询。
class Solution {int son[1010][26],idx;bool cnt[1010] = {0};
public:void insert(string x){int p = 0,len = x.length();for(int i=0;i<len;i++){int u = x[i] - 'a';if(!son[p][u]) son[p][u] = ++idx;p = son[p][u];}cnt[p] = true;}bool query(string x){int p = 0,len = x.length();for(int i=0;i<len;i++){int u = x[i] - 'a';if(!son[p][u]) return false;p = son[p][u];}return cnt[p];}vector<vector<int>> indexPairs(string text, vector<string>& words) {vector<vector<int>> res;int sz = words.size();for(int i = 0;i<sz;i++){insert(words[i]);}int len = text.length();for(int i = 0;i<len;i++){for(int j = 1;j<=len-i;j++){string str = text.substr(i,j);cout << str << endl;if(query(str)) res.push_back({i,i+j-1});}}return res;}
};
(6) Leetcode 14.最长公共前缀
讲道理其实没啥难度。。想了好一会来着。。用字典树去做其实复杂了。。。不过是字典树专题。。
对每一个字符串先查询再存储,因为我们要找的是整个数组中字符串的公共前缀,因此我们每次查询时都需要把结果更新成最短的那个公共前缀。同时要注意特殊情况(一个字符串),第一个字符串最好先插入。
class Solution {int son[40010][26],idx;
public:void insert(string x){int p = 0,len = x.length();for(int i = 0;i<len;i++){int u = x[i] - 'a';if(!son[p][u]) son[p][u] = ++idx;p = son[p][u];}}int query(string x){int p = 0,len = x.length(),res = 0;for(int i = 0;i<len;i++){int u = x[i] - 'a';if(!son[p][u]) return res;p = son[p][u];res++;}return res;}string longestCommonPrefix(vector<string>& strs) {int sz = strs.size();int res = 100000;string ans = strs[0];insert(strs[0]);for(int i = 1;i<sz;i++){if(query(strs[i]) < res){res = query(strs[i]);ans = strs[i].substr(0,res);if(ans == "") return ans;}insert(strs[i]);}return ans;}
};
(7) Leetcode 139.单词拆分
其实跟Trie没啥关系(因为要Trie+dfs不会。。。)用dp做的,贴上来是为了羞辱一下自己的菜。
dp[i]指前i个字母是否能被拼凑出来,枚举前i个字母的每一个插空位j,对插空位j前面的字符串,其能否被拼凑可看dp[j],对j后面的字符,利用check函数(截取字符串)判断是否能在哈希set中找到。对于初始状态,规定dp[0] = true。
class Solution {bool dp[310];unordered_set<string> res;
public:bool check(string x){if(res.find(x) != res.end()) return true;return false;}bool wordBreak(string s, vector<string>& wordDict) {int sz = wordDict.size();for(int i = 0;i<sz;i++) res.insert(wordDict[i]);int n = s.length();dp[0] = true;for(int i = 1;i<=n;i++){for(int j = 0;j<i;j++){if(dp[j] && check(s.substr(j,i-j))){dp[i] = true;break;}}}return dp[n];}
};
(8) Leetcode 720.词典中最长的单词
无非就是打打标记嘛。。。and注意审题。。。
要求其他单词“逐步”添加一个字母构成的最长单词,那么对query查找进行改装即可,只遍历数组的前n-1个字母,每一个字母处如果都有标记则符合要求。
class Solution {int son[30010][26],idx;bool cnt[30010];
public:void insert(string x){int p = 0,len = x.length();for(int i = 0;i<len;i++){int u = x[i] - 'a';if(!son[p][u]) son[p][u] = ++idx;p = son[p][u];}cnt[p] = true;}bool query(string x){int p = 0,len = x.length();if(len == 1) return true;for(int i = 0;i<len-1;i++){int u = x[i] - 'a';if(!son[p][u]) return false;p = son[p][u];if(!cnt[p]) return false;}return cnt[p];}string longestWord(vector<string>& words) {int n = words.size();for(int i = 0;i<n;i++){insert(words[i]);}string ans = "";int res = 0;for(int i = 0;i<n;i++){int len = words[i].length();string str = words[i];if(query(str) && (res < len|| (res == len && str < ans))){ans = str;res = len;}}return ans;}
};
(9) Leetcode 1268.搜索推荐系统
一开始就没想出来。。且坑点巨多。。改也改了好久。。
这道题对字典树的“节点”这个概念有很深的强化。对每一个节点,我们对应一个字符串的大根堆,每次访问这个节点时把当前字符串加入,并且如果该大根堆中字符串数目大于3时,将堆顶的字符串(字典序最大的那个)弹出。对于目标字符串,我们一个节点一个节点进行访问,如果遇到了无相应子节点的节点,则后面全部为空集,这里我们需要一个bool变量来记录是否已经出现该情况,如果只用continue判断会出错(p=0时也意味着p回到根节点)。由于每个节点对应着大根堆,因此每个节点得到的集合还需要取反。
class Solution {int son[200010][26],idx = 0;unordered_map<int,priority_queue<string>> y;
public:void insert(string x){int p = 0,len = x.length();for(int i = 0;i<len;i++){int u = x[i] - 'a';if(!son[p][u]) son[p][u] = ++idx;p = son[p][u]; y[p].push(x);if(y[p].size() > 3) y[p].pop();}}vector<vector<string>> query(string x){vector<vector<string>> res;int p = 0,len = x.length();bool flag = false;for(int i = 0;i<len;i++){int u = x[i] - 'a';if(flag || !son[p][u]){res.push_back({});flag = true;}else{p = son[p][u]; vector<string> t;while(y[p].size()){t.push_back(y[p].top());y[p].pop();}reverse(t.begin(),t.end());res.push_back(t);}}return res;}vector<vector<string>> suggestedProducts(vector<string>& products, string searchWord) {int n = products.size();for(int i = 0;i<n;i++){insert(products[i]);}return query(searchWord);}
};
(10) Leetcode 2707.字符串中的额外字符
。。DP做不出啊啊啊啊啊。。。
记n为字符串长度,我们对n([0,n-1])有两种划分方式,一种是把 s[n-1]当做额外字符,因此dp[n] = dp[n-1] + 1,另一种是查看[j,n-1]是否在字典中可查找到,如果可以则dp[n]又等于dp[j](相当于[j,n-1]已经处理好,只需要再处理[0,j-1])因此对每一个i枚举对应的j即可。
class Solution {int d[60],son[2510][26],idx;bool cnt[2510];
public:void insert(string x){int p = 0,len = x.length();for(int i = 0;i<len;i++){int u = x[i] - 'a';if(!son[p][u]) son[p][u] = ++idx;p = son[p][u];}cnt[p] = true;}bool query(string x){int p = 0,len = x.length();for(int i = 0;i<len;i++){int u = x[i] - 'a';if(!son[p][u]) return false;p = son[p][u];}return cnt[p];}int minExtraChar(string s, vector<string>& dictionary) {int n = dictionary.size(),len = s.length();for(int i = 0;i<n;i++){insert(dictionary[i]);}d[0] = 0;n = s.length();for(int i = 1;i<=s.length();i++){d[i] = d[i-1] + 1;for(int j = 0;j<i;j++){if(query(s.substr(j,i-j))) d[i] = min(d[i],d[j]);}}return d[n];}
};