字典树
食用指南:
对该算法程序编写以及踩坑点很熟悉的同学可以直接跳转到代码模板查看完整代码
只有基础算法的题目会有关于该算法的原理,实现步骤,代码注意点,代码模板,代码误区的讲解
非基础算法的题目侧重题目分析,代码实现,以及必要的代码理解误区
题目描述:
-
维护一个字符串集合,支持两种操作:
I x 向集合中插入一个字符串 x;
Q x 询问一个字符串在集合中出现了多少次。
共有 N 个操作,输入的字符串总长度不超过 105,字符串仅包含小写英文字母。输入格式
第一行包含整数 N,表示操作数。
接下来 N 行,每行包含一个操作指令,指令为 I x 或 Q x 中的一种。输出格式
对于每个询问指令 Q x,都要输出一个整数作为结果,表示 x 在集合中出现的次数。
每个结果占一行。数据范围
1≤N≤2∗104
输入样例:
5
I abc
Q abc
Q ab
I ab
Q ab
输出样例:
1
0
1 -
题目来源:https://www.acwing.com/problem/content/837/
题目分析:
-
字典树:
字典树只支持两种操作:1,插入字符串;2,查询字符串插入时遍历树节点插入,查询时遍历树节点查询,两个操作其实是同一个
-
下面将介绍树结构-字典树(Trie)
算法原理:
模板算法:
- 传送门:静态单链表
字典树Trie:
1. 字母库:
- 由于我们一共插入N个节点,每个节点可能有26个字母
所以开一个二维数组:tree[N][26] - 每到一个节点就从字母库中取走一行,即26格
1. 存储形式:
-
字典树树根:
空节点,idx=0,存在就是为了便于遍历树 -
树上节点:
节点本身存储的是节点的idx序号,但是节点抽取字母库中的一行时,抽取的是tree[idx][26] -
支路数组:
每个idx节点对应的tree[idx]行都包含着26个元素,视作从idx节点出发有26条支路tree[i][j] == k表示从i节点沿着第j个支路路径到达了k节点,则k节点代表字母的是’a’+j
换句话说:tree[当前节点的idx][支路a~z] = 子节点的idx
-
idx序号:
idx相邻代表的是物理上抽取字母库中相邻的两行树上节点的相邻判断依靠的支路数组tree[idx][j] == k,则idx的子节点是k
-
终结数组:
字典树上有很多idx节点,我们遍历时如何判断一个字符串已经读取完全?
当输入完整一个字符串后,我们将字符串最后一个字母对应的节点idx做一个标记
将end[idx] = 1;则说明此处有一字符串结束
2. 算法核心:
- 字典树上的节点本身存储的是idx序号
- 每个idx序号都分出26条支路
- 从树根的空节点开始遍历,当支路数组的x大于0,说明子节点代表字母’a‘+x
- 遍历过程中依靠的是支路数组的索引来输出字符串
- idx仅仅起到连接树上节点的作用
- 遍历到一节点的idx对应end[idx]=1时,表明一个字符串结束
3. 举例说明:
-
现在向字典树中插入三个字符串:abc,abcd,adc
-
从树根idx=0开始插入,以插入abcd 和 acd为例:
用指针p遍历树:p=0
tree[p][‘a’-‘a’] == 0; 该路径未有节点 则tree[p][‘a’-‘a’] = ++idx; p=idx;//创建并到达了第一个实节点
tree[p][‘b’-‘a’] == 0; 该路径未有节点 则tree[p][‘b’-‘a’] = ++idx; p=idx;//创建并到达了第二个实节点
tree[p][‘c’-‘a’] == 0; 该路径未有节点 则tree[p][‘c’-‘a’] = ++idx; p=idx;//创建并到达了第三个实节点
tree[p][‘d’-‘a’] == 0; 该路径未有节点 则tree[p][‘d’-‘a’] = ++idx; p=idx;//创建并到达了第四个实节点
abcd插入完成,end[p] = 1;开始从树根idx=0插入acd,p重置为0
tree[p]/[‘a’-‘a’] == 1 说明空节点到a的路径已经存在,用变量p沿着路走 p =
tree[p]/[‘d’-‘a’] == 0 该路径未有节点 则tree[p][‘d’-‘a’] = ++idx; p = idx; //创建并到达了新节点
tree[p]/[‘c’-‘a’] == 0 该路径未有节点 则tree[p][‘c’-‘a’] = ++idx; p = idx; //创建并到达了新节点
adc插入完成,end[p] = 1;
4. 插入&查询字符串:
- 插入时从根节点按照字符串路径遍历树,
遇到某点对应的支路为空,则在支路端++idx创建节点,并到达该节点继续遍历
插入完成后将结尾的节点idx对应end[idx]置为1 - 查询时从根节点按照字符串路径遍历树
遇到某点对应支路为空,则不存在该字符串;
遍历完毕该字符串end[idx] 为0,则仍然不存在该字符串 - 给定了字符串相当于给定了路径
和文件管理器路径一样:
写作步骤:
1. 初始化:
- 字符库必须要tree[N][26];
- 字符库中每一行同时记录支路对应的下一节点idx
初始树根无下一节点,将支路设为树根自己的idx == 0; - 初始无节点,end[]均为0
2. 从树根开始遍历:
- 利用单个指针p = 0;遍历节点
3. 选择支路:
- 已经给定字符串,则给定了路径
- 字符串中每个字母都是一条支路,tree[idx][‘x’ - ‘a’];
4. 从支路继续遍历:
- 若tree[idx][‘x’-‘a’] == 0,说明这条支路无节点,插入时需要创建节点 ++idx;
- 若tree[idx][‘x’-‘a’] == k,说明这条支路有节点,p置为这条支路的节点,p=k;
5. 结束标志:
- 插入时,插入完成将结尾节点idx对应end[idx] = 1;
- 查询时,就算遍历完成了整个字符串,最后也要看看结尾end[idx]是否为1
代码实现:
- 插入和查询的核心都是遍历
const int N = 100010;
int idx = 0;
int tree[N][26], end[N];
char str[N];
void insert(char str[]){
//从根开始
int p = 0;
for(int i=0; str[i]; i++){
//选择支路
int u = str[i] - 'a';
if(!tree[p][u])
tree[p][u] = ++idx;
//从支路继续
p = tree[p][u];
}
//结束标记
end[p]++;
}
int search(char str[]){
//从根节点开始
int p = 0;
for(int i=0; str[i]; i++){
//选择支路
int u = str[i] - 'a';
if (!tree[p][u]){
return 0;
}
//从支路继续
p = tree[p][u];
}
//确定是否结尾
return end[p];
}
代码误区:
1. 字典树中每个节点的字母如何确定?
- 每个节点的字母表示需要看他的父节点通过哪条支路到达该节点
- 即tree[父节点idx][字母] = 子节点idx
2. 字典树中的节点存储的是什么?
- 逻辑上存储的是通过哪个字母支路到达的该节点
- 物理上存储的是该节点的idx序号
3. 字典树中字母库的作用:
- 一方面,每一个节点都需要26条字母支路与其连接,虽然有的字母支路是空。所以每个节点要开一个arr[26]
- 另一方面,需要用arr[26]标记对应字母支路到达的是哪个节点,如arr[‘b’-‘a’] = k;表示通过b支路到达k节点,k节点逻辑存储着b字母
- 最后一方面,一个字典树上有许多节点,所以不只需要一个arr[26],故开一个大的二维数组arr[N][26]
4. 给定的字符串的作用:
- 字符串就是路径,是我们从树根遍历时选择支路的依据
本篇感想:
- 看完本篇博客,恭喜已登 《练气境-后期》
预计35篇左右到达图论,对dfs bfs不熟悉的同学看这里:【算法设计】用C++类和队列实现图搜索的广度优先遍历算法
距离登仙境不远了,加油