P8306 【模板】字典树
题目描述
给定 nnn 个模式串 s1,s2,...,sns_1, s_2, \dots, s_ns1,s2,...,sn 和 qqq 次询问,每次询问给定一个文本串 tit_iti,请回答 s1∼sns_1 \sim s_ns1∼sn 中有多少个字符串 sjs_jsj 满足 tit_iti 是 sjs_jsj 的前缀。
一个字符串 ttt 是 sss 的前缀当且仅当从 sss 的末尾删去若干个(可以为 000 个)连续的字符后与 ttt 相同。
输入的字符串大小敏感。例如,字符串 Fusu 和字符串 fusu 不同。
输入格式
本题单测试点内有多组测试数据。
输入的第一行是一个整数,表示数据组数 TTT。
对于每组数据,格式如下:
第一行是两个整数,分别表示模式串的个数 nnn 和询问的个数 qqq。
接下来 nnn 行,每行一个字符串,表示一个模式串。
接下来 qqq 行,每行一个字符串,表示一次询问。
输出格式
按照输入的顺序依次输出各测试数据的答案。
对于每次询问,输出一行一个整数表示答案。
输入输出样例 #1
输入 #1
3
3 3
fusufusu
fusu
anguei
fusu
anguei
kkksc
5 2
fusu
Fusu
AFakeFusu
afakefusu
fusuisnotfake
Fusu
fusu
1 1
998244353
9
输出 #1
2
1
0
1
2
1
说明/提示
数据规模与约定
对于全部的测试点,保证 1≤T,n,q≤1051 \leq T, n, q\leq 10^51≤T,n,q≤105,且输入字符串的总长度不超过 3×1063 \times 10^63×106。输入的字符串只含大小写字母和数字,且不含空串。
说明
std 的 IO 使用的是关闭同步后的 cin/cout,本题不卡常。
代码
cpp
#include <bits/stdc++.h>
using namespace std;
struct node{
char c;//字符
unordered_map<char,node*> hashx;//哈希表,子节点映射表,字符->子节点指针
node *fa;
bool end;//字符串尾标记
int tot;//有多少个分支
node(char x=0):c(x),end(0),tot(0),fa(NULL){}//默认参数初始化节点
};
class trie{
private:
node* root;//私有变量根节点
void destroy(node *cur){//递归销毁字典树占有内存
for(unordered_map<char,node*>::iterator it=cur->hashx.begin();it!=cur->hashx.end();it++)//遍历节点的哈希表
destroy(it->second);//先销毁该节点子节点,
delete cur;//再删除当前节点
}
bool deletex(const string& s,node *&cur,int idx){//删除字典树中的当前字符串
if(s.size()<=idx){//递归基,遍历完了该字符串
if(!cur->end)return 0;//无结束标记,不删
cur->end=0;//否则取消结束标记
return cur->hashx.empty();//空了,无子节点,可删
}
if(cur->hashx.find(s[idx])==cur->hashx.end())return 0;//无该字符,不删
bool k=deletex(s,cur->hashx[s[idx]],idx+1);//递归查询子节点,返回值决定该节点能不能删
if(k){//可以删
delete cur->hashx[s[idx]];//删除该节点idx对应子节点
cur->hashx.erase(s[idx]);//从本节点哈希表中删除该元素
return cur->hashx.empty()&&!cur->end;//当前节点无后代节点且非尾节点,则可删
}
return 0;//意思是子节点不可删,当前节点也保留。没有也正确。
}
public:
trie(){root=new node();}//初始化时完成私有变量root的初始化
~trie(){destroy(root);}//析构函数,释放内存
void insert(const string& s){//往字典树插入字符串s
node *fa=root,*cur;
int i=0;
for(;i<s.size();i++){
if(fa->hashx.find(s[i])==fa->hashx.end()){//如果没该字符就往哈希表添加元素,并指向对应节点
cur=new node(s[i]);
cur->fa=fa;
fa->hashx[s[i]]=cur;
}
fa=fa->hashx[s[i]];//进入下一层节点
fa->tot++;
}
fa->end=1;//标记字符串尾标记
}
bool search(const string& s){//查询字典树中是否有该字符串
node *cur=root;//根节点
for(int i=0;i<s.size();i++){
if(cur->hashx.find(s[i])==cur->hashx.end())return 0;//没有该字符
cur=cur->hashx[s[i]];//查询下个字符
}
return cur->end;//判断是否有完整字符串尾标记
}
int count_pre(const string& s){//查询字典树中是否有该字符串
node *cur=root;//根节点
for(int i=0;i<s.size();i++){
if(cur->hashx.find(s[i])==cur->hashx.end())return 0;//没有该字符
cur=cur->hashx[s[i]];//查询下个字符
}
return cur->tot;//判断是否有完整字符串尾标记
}
bool remove(const string& s){//从字典树中删除该字符串
return deletex(s,root,0);//调用私用函数完成删除
}
void view(){//层序打印该字典树
cout<<"字典树\n";
node *cur=root;//根节点
int cur_level=0;//当前层数
queue<pair<node*,int>> q; q.push({cur,cur_level});//宽搜队列,并初始化
while(!q.empty()){//层序遍历循环
int level=q.front().second;//该节点深度
cur=q.front().first;q.pop();//队首元素
if(cur_level<level){cout<<endl;cur_level=level;}//深度变化则换行
for(unordered_map<char,node*>::iterator it=cur->hashx.begin();it!=cur->hashx.end();it++){//节点内哈希表的每元素
if(it->second->end)cout<<'*';
cout<<it->second->c<<","<<it->second->tot<<"\t";q.push({it->second,level+1});//输出容器内元素第二部分------节点,带深度插入队列
}
}
cout<<"________________\n";
}
};
string s;
int t,
n,
q;
int main(){
//freopen("data.cpp","r",stdin);
cin>>t;
while(t--){
trie pre;//本生命周期结束就运行析构函数,自动销毁
cin>>n>>q;
for(int i=0;i<n;i++){
cin>>s;
pre.insert(s);
//pre.view();
}
for(int i=0;i<q;i++){
cin>>s;
cout<<pre.count_pre(s)<<endl;
}
}
return 0;
}
trie字典树的优势
1.极致的前缀匹配效率,字符串插入、查询的时间复杂度仅与查询字符串的长度有关。O(len)
2.空间高效的前缀压缩,通过共享公共前缀大幅节省存储空间。
3.支持字典序遍历,无需额外排序。
4.扩展能力强,适合大规模字符串集合。