例题:(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];
}
};