trie 树详解 + 例题
看这篇做的笔记la~
ほら、もうすぐ晴れますよ!
字典树
字典树(trie 树)是一种用于实现字符串快速检索的多叉树结构。
trie 树的每个节点都拥有若干个字符指针,若在插入或检索字符串时扫描到一个字符 c,就沿着当前节点的 c 字符指针,走向该指针指向的节点。
下图即为一个简易版字典树,存储了单词:abc、bac、abd。
图有点奇怪请见谅
实现:
初始化:
一棵空的 \(trie\) 树只包含一个根节点,该字符的指针均指向空。
不过字符还需要转换成一个数字,这里我们就需要用到一个类似于map映射的东西
struct node{
int cnt=0; //cnt 表示到这个节点为止,一共有多少个前缀
int son[65]; // 每个节点的分支
}z[maxn];
int getnumber(char ch)//把字符转换成数字
{
if(ch>='A'&&ch<='Z') return ch-'A';
if(ch>='a'&&ch<='z') return ch-'a'+26;
return ch-'0'+52;
}
插入:
当需要插入一个字符串 \(s\) 时,我们令一个指针 \(P\) 起初指向根节点。然后依次扫描 \(s\) 中的每一个字符 \(c\)
- 若 \(P\) 的 \(c\) 字符指针指向一个已经存在的节点 \(Q\) ,则令 \(P=Q\)
- 若 \(P\) 的 \(c\) 字符指针指向空,则新建一个节点 \(Q\) 令 \(P\) 的 \(c\) 字符指针指向 \(Q\) ,然后令 \(P=Q\)
- 当 \(s\) 中的字符扫描完毕,在当前节点 \(P\) 上标记它是一个字符串的末尾
void insert(string s) //插入字符串 s
{
int now=1;//从根节点开始查找
for(int i=0;i<s.length();i++) //分解每一个字符
{
int num=getnumber(s[i]);
if(!z[now].son[num]) //从 now 出发没有这个字符
z[now].son[num]=++cnt;//添加这条边和这条边连向的节点
//z[now].cnt++;//到这里有前缀
now=z[now].son[num];//沿着这条边查找下一个字符
}
z[now].cnt++;
//若已经是字符串的最后一个字符,则代表字典树的这个节点是一个单词的末尾,统计的cnt需要+1
return ;
}
检索:
当需要检索一个字符串 \(s\) 在 \(Trie\) 中是否存在时,我们令一个指针 \(P\) 起初指向根节点,然后依次扫描 \(s\) 中的每个字符 \(c\)。
- 若 \(P\) 的 \(c\) 字符指针指向空,则说明 \(S\) 没有被插入到 Trie 树中,结束检索;
- 若 \(P\) 的 \(c\) 字符指针指向一个已经存在的节点 \(Q\) ,则令 \(P=Q\)
- 若在当前节点 \(P\) 被标记为一个 字符串的末尾,则说明 \(S\) 在 \(Trie\) 树中存在,否则说明 \(S\) 没有被插入过
void query(string s)
//查找字符串 s 在 trie 中是否存在
{
int now=1;
for(int i=0;i<s.length();i++)
{
int num=getnumber(s[i]);
if(z[now].son[num]==0)
{
cout<<0<<endl;//表示不存在
return;
}
now=z[now].son[num];
}
cout<<z[now].cnt<<endl;
return;
}
例题:
P8306 【模板】字典树
这个输入我看了好久才明白 qwq
- 题目意思:
给你 \(n\) 个字符串 \(si\),再给你 \(q\) 次询问,每次询问给你一个字符串 \(ti\),求出 \(ti\) 上面 \(n\) 个 \(si\) 中多少个的前缀。
一个字符串 \(t\) 是 \(s\) 的前缀当且仅当从 \(s\) 的末尾删去若干个(可以为 0 个)连续的字符后与 \(t\) 相同。
- 思路:
让每个节点的 cnt 表示到达这个节点的字符串数量,每次询问查找 \(ti\) 的最后一个字符所在的节点的 cnt 值。 - 代码:
#include<bits/stdc++.h>
using namespace std;
int T;
int n,q;
int cnt,start;
struct node{
int son[65];//大小写敏感
int cnt;//表示到达这个点的字符串有多少
}z[3000100];
int getnumber(char ch)
{
if(ch>='a'&&ch<='z')return ch-'a'+1;
//小写字母占据 1~26 的数组范围
else if(ch>='A'&&ch<='Z')return ch-'A'+27;
//大写字母占据 27~52 的数组范围
else return ch-'0'+53;
//数字占据 53~62 的数组范围
}
void insert(string s)
{
int now=start;
for(int i=0;i<s.length();i++)
{
int num=getnumber(s[i]);
if(!z[now].son[num])
z[now].son[num]=++cnt;
z[now].cnt++;
now=z[now].son[num];
}
z[now].cnt++;
return;
}
void query(string t)
{
int now=start;
for(int i=0;i<t.length();i++)
{
int num=getnumber(t[i]);
if(!z[now].son[num]){
cout<<0<<endl;
return;
}
now=z[now].son[num];
}
cout<<z[now].cnt<<endl;
return;
}
int main()
{
cin>>T;
while(T--) //因为有多组测试数据
//每次查询完都做一下清空太麻烦了
//我们可以每次重新选一个root,那就用原来的节点数 +1
{
cin>>n>>q;
for(int i=1;i<=n;i++)
{
string s;
cin>>s;
insert(s);
}
for(int i=1;i<=q;i++)
{
string t;
cin>>t;
query(t);
}
start=++cnt;
}
}
P10470 前缀统计
- 题目意思:
这个题意思挺清晰的,就不重复了。 - 思路:
其实就是把上一个题目倒过来了?
那我们就换个实现方法。
其实只需要改动一个地方:cnt 的含义。
我们可以把所有的 \(si\) 放入 \(Trie\) 中,每个字符串的末尾节点的cnt+1,这样 cnt 的含义变成:有多少个 \(si\) 在这里结尾。
每次查询时,答案就把 \(Ti\) 所经过的所有节点的 cnt 值加起来。 - 代码:
#include<bits/stdc++.h>
using namespace std;
int n,m;
int cnt=1; //我也不知道为啥这里要是 1
struct node{
int son[30];//只有小写
int cnt;//表示到这个点结束的字符串有多少
}z[1000100];
int getnumber(char ch)
{
return ch-'a'+1;
}
void insert(string s)
{
int now=1;
for(int i=0;i<s.length();i++)
{
int num=getnumber(s[i]);
if(!z[now].son[num])
z[now].son[num]=++cnt;
//z[now].cnt++;//这里就不需要加了
now=z[now].son[num];
}
z[now].cnt++;
return;
}
void query(string t)
{
int ans=0;
int now=1;
for(int i=0;i<t.length();i++)
{
int num=getnumber(t[i]);
if(!z[now].son[num]){
cout<<ans<<endl;
return;
}
now=z[now].son[num];
ans+=z[now].cnt;
}
cout<<ans<<endl;
return;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
string s;
cin>>s;
insert(s);
}
for(int i=1;i<=m;i++)
{
string t;
cin>>t;
query(t);
}
return 0;
}
完结撒花! 总算学完了
感觉还挺简单的?