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

  • Pc 字符指针指向一个已经存在的节点 Q ,则令 P=Q
  • Pc 字符指针指向空,则新建一个节点 QPc 字符指针指向 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 ;
}

检索:

当需要检索一个字符串 sTrie 中是否存在时,我们令一个指针 P 起初指向根节点,然后依次扫描 s 中的每个字符 c

  • Pc 字符指针指向空,则说明 S 没有被插入到 Trie 树中,结束检索;
  • Pc 字符指针指向一个已经存在的节点 Q ,则令 P=Q
  • 若在当前节点 P 被标记为一个 字符串的末尾,则说明 STrie 树中存在,否则说明 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 上面 nsi 中多少个的前缀。

一个字符串 ts 的前缀当且仅当从 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;
}

完结撒花! 总算学完了

感觉还挺简单的?

posted @   lazy_ZJY  阅读(25)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示