trie 树详解 + 例题
看这篇做的笔记la~
ほら、もうすぐ晴れますよ!
字典树
字典树(trie 树)是一种用于实现字符串快速检索的多叉树结构。
trie 树的每个节点都拥有若干个字符指针,若在插入或检索字符串时扫描到一个字符 c,就沿着当前节点的 c 字符指针,走向该指针指向的节点。
下图即为一个简易版字典树,存储了单词:abc、bac、abd。
图有点奇怪请见谅
实现:
初始化:
一棵空的
不过字符还需要转换成一个数字,这里我们就需要用到一个类似于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; }
插入:
当需要插入一个字符串
- 若
的 字符指针指向一个已经存在的节点 ,则令 - 若
的 字符指针指向空,则新建一个节点 令 的 字符指针指向 ,然后令 - 当
中的字符扫描完毕,在当前节点 上标记它是一个字符串的末尾
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 ; }
检索:
当需要检索一个字符串
- 若
的 字符指针指向空,则说明 没有被插入到 Trie 树中,结束检索; - 若
的 字符指针指向一个已经存在的节点 ,则令 - 若在当前节点
被标记为一个 字符串的末尾,则说明 在 树中存在,否则说明 没有被插入过
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
- 题目意思:
给你 个字符串 ,再给你 次询问,每次询问给你一个字符串 ,求出 上面 个 中多少个的前缀。
一个字符串
是 的前缀当且仅当从 的末尾删去若干个(可以为 0 个)连续的字符后与 相同。
- 思路:
让每个节点的 cnt 表示到达这个节点的字符串数量,每次询问查找 的最后一个字符所在的节点的 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 的含义。
我们可以把所有的 放入 中,每个字符串的末尾节点的cnt+1,这样 cnt 的含义变成:有多少个 在这里结尾。
每次查询时,答案就把 所经过的所有节点的 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; }
完结撒花! 总算学完了
感觉还挺简单的?
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?