一、什么是字典树
我之前发布了关于 KMP 算法实现的文章,Trie树和KMP算法都是用于字符串匹配的算法,它们可以在一定程度上互相补充和优化。👇👇👇
洛谷P3375 【模板】KMP_洛谷 p3375-CSDN博客https://blog.csdn.net/ByteMaster_/article/details/141028724?spm=1001.2014.3001.5501KMP 算法的原理在上述博客中已经讲清,本篇博客用来讲解trie树算法
字典树 正如它字面意思,把字符串按照字典的顺序存放在一棵树上,通过每个单词和其他单词的公共前缀把他们连接起来,如图所示
如图所示
5 : apple 12 : application 15 : appear 18 : cat 24 : capaticity 27 : bus 32 : banana
二、如何构建字典树
构建这样的字典多叉树 可以用来进行 快速插入和查询字符串的操作
节点的编号各不相同,根节点的编号为0 ,其他节点用来标识路径 还可以用来记录插入单词的次数, 边用来表示具体的字符
trie树用来维护字符串的集合 支持两种操作
1.向集合中插入一个字符串,void insert(string s)
2.在集合中查询一个字符串,int query(string s)
下面我们来看如何用代码实现此过程
1、insert的实现
insert的实现,需要用到
①儿子数组 ch[p][j]
代表的是存储从节点p沿着j这条边走到的节点
ch[i][p]中 p 的值代表的是 26 个小写字母的映射(a~z,用它们的ASCII值减去‘a'表示)
例如 ch[0][2]=16 代表的就是原图中的 从 0 这个节点走c这条边(c的映射值是2)走到16这个节点
或者说是 节点 0 和 节点 16 之间的字符是 'c'
②计数数组 cnt[p] 代表以p为结尾的单词的插入次数
③节点编号 idx 用来给每个节点编号
插入单词的过程如下
1.空的 trie 树的节点为 0
2.指针p按照所要插入的单词的每一个字符的顺序在trie树上游历, 如果对应的此个儿子节点不为 0 (即),此时证明 当前p所在位置 与 当前字符对应的s[i]-'a'这条边 所连接的儿子已经存在,无需再插入,p走向该儿子
比如在已经插入好 cat 时,想插入 cate ,指针 p 指向2这个节点,然而ch[2][19]已经为3(2和3之间有 t 的映射 19 这条边),此时 p 直接指向 3 进行下一步即可,无需再创建新的儿子了
3.若对应的儿子节点为0(即 ),证明当前 p 所在位置 与 当前字符对应的s[i]-'a'这条边 所连接的儿子节点还不存在,创建这个儿子节点,后走向儿子节点
此时 p 指向 3 这个节点,由于ch数组的初始化为0 所以ch[3][4]的值为0 ,说明当前儿子不存在,所以需要做的就是,创建儿子,也就是 令 ch[3][4] 为4(节点3 和 节点4 之间有 e 的映射 4 这条边),然后
4.再用cnt数组记录插入单词的次数
代码实现如下
void insert(string s)
{
int p = 0;
for (int i = 0; i < s.size(); i++)
{
int j = s[i] - 'a';//每个字符的映射值
if (trie[p][j] == 0) trie[p][j] = ++id;//创建儿子,且id加1
p = trie[p][j];//p走向儿子节点
}
cnt[p]++;//该单词记录完,cnt[p]加1,表示单词次数
}
2、query的实现
query用来查询这个字符串的出现次数,基本逻辑与 insert 类似
也是一个 p 指针在多叉树上游历,按照字符串 s 的每个字符的内容看 ch[p][s[i]-'a'] 的值,
1.ch[p][s[i]-'a'] 为 0 说明该单词在p所在位置处即断开,没构成完整的目标 s 字符串,所以cnt的值为0 此时直接返回 0 代表没有出现过目标单词
2.如果 ch[p][s[i]-'a'] 不为 0 说明当前目标字符串 在p所在位置对应的字符已经存在,p走向下一个位置(当前节点的下一个节点),继续重复判断
3.最后返回 cnt[p] ,p走向最终位置 ,cnt[p]为该单词的出现次数
代码实现如下
int query(string s){
int p = 0;
for(int i =0; i<s.size();i++){
int j = s[i]-'a';//当前字符的映射值
if (ch[p][j] == 0) return 0;//此单词不存在
p = ch[p][j];//指向下一个位置
}
return cnt[p];//返回出现次数
}
三、洛谷 P3879 [TJOI2010] 阅读理解
模板题
P3879 [TJOI2010] 阅读理解 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)https://www.luogu.com.cn/problem/P3879
题目描述
英语老师留了 N 篇阅读理解作业,但是每篇英文短文都有很多生词需要查字典,为了节约时间,现在要做个统计,算一算某些生词都在哪几篇短文中出现过。
输入格式
第一行为整数 N ,表示短文篇数,其中每篇短文只含空格和小写字母。
按下来的 N 行,每行描述一篇短文。每行的开头是一个整数 L ,表示这篇短文由 L 个单词组成。接下来是 L 个单词,单词之间用一个空格分隔。
然后为一个整数 M ,表示要做几次询问。后面有 M 行,每行表示一个要统计的生词。
输出格式
对于每个生词输出一行,统计其在哪几篇短文中出现过,并按从小到大输出短文的序号,序号不应有重复,序号之间用一个空格隔开(注意第一个序号的前面和最后一个序号的后面不应有空格)。如果该单词一直没出现过,则输出一个空行。
输入输出样例
输入 #1
3 9 you are a good boy ha ha o yeah 13 o my god you like bleach naruto one piece and so do i 11 but i do not think you will get all the points 5 you i o all naruto输出 #1
1 2 3 2 3 1 2 3 2说明/提示
对于 30%的数据, 1≤M≤10^3 1≤M≤10^3 。
对于 100%的数据,1≤M≤10^41≤M≤10^4,1≤N≤10^31≤N≤10^3 。
每篇短文长度(含相邻单词之间的空格)≤5×10^3字符,每个单词长度 ≤20 字符。
每个测试点时限 2 秒。
根据代码的模块化理论,我们可以在insert函数和query中增加上输入功能,使得代码看着更加整洁。
通过阅读题目,我们可以知道 共输入 N 篇文章,然后最后询问 M 次,那么我们需要想办法标记下是第几篇文章出现的我们需要查找的单词,从上述的trie树的模板代码中,我们看到有个数组就是用来记录单词出现的次数(以及位置)的,那就是 cnt 数组
那么我们怎么可以让 cnt 数组在可以记录单词出现次数(以及位置)的基础上,再记录下单词在哪一篇文章出现呢,那就需要给 cnt 数组增加一维,增加的那一维用来记录单词所出现的文章的序号
即 cnt[p][t]==1 代表单词末尾的序号为 p ,它在 第 t 篇文章中出现
有了这个,我们就解决了单词在哪一篇文章中出现的问题
//P3879 [TJOI2010] 阅读理解
#include <bits/stdc++.h>
using namespace std;
int N,L,M;
int trie[500010][26];
bool cnt[500010][1333];
string s;
int id=0;//⭐
void insert(int t){
cin>>s;
int p = 0;
for(int i = 0;i<(int)s.length();i++){
int x =s[i]-'a';
if(trie[p][x]==0) trie[p][x]=++id;
p = trie[p][x];
}
cnt[p][t]=1;//单词再在第t篇文章出现
}
void find (){
cin>>s;
int p = 0,f=1;//f用来标记
int l = s.length();
for(int i =0;i<l;i++){
int x = s[i]-'a';
if(trie[p][x]==0){
f=0;//如果f等于0 代表单词没有在文章中找到 或者找到了一部分
//剩下的断了
break;
}
p = trie[p][x];
}
if(f){//只有f==1时,代表单词在所有文章中出现过
for(int i = 1;i<=N;i++){
if(cnt[p][i]==1)
cout<<i<<" ";
}
}
cout<<endl;//f==0时 表明单词没有出现过 直接跳转到这 输出换行
}
int main(){
cin>>N;//N篇文章
for(int i = 1;i<=N;i++){
cin>>L;//L个单词
for(int j = 1 ;j<=L;j++){
insert(i);
//在第i篇文章中依次输入L个单词
//这是第 j 个
}
}
cin>>M;
for(int i = 1;i<=M;i++){
find();//查找单词 在find内部输入
}
return 0;
}