一. 字典树(Trie 树)基础概念
字典树是一种树形结构,用于高效地存储和检索字符串数据集 中的键,也叫前缀树。它的每个节点代表一个字符,从根节点到某一节点的路径上的字符连接起来,就形成了一个字符串前缀。比如存储字符串 "apple""app""banana","app" 这部分路径是共享的。

二. 字典树在 C++ 中的实现
以下是在 C++ 中实现字典树的关键部分:
定义数据结构:
const int N = 1e5 + 10;
int ch[N][26], cnt[N], idx;
// ch表示树,ch[p][j] 表示存储从节点p沿j这条边走到下一子节点(j 取值 0 - 25 对应26个英文字母)
// 计数数组cnt[p] 存储以节点p结尾的单词的插入计数
// 节点编号idx,用来给新节点编号
插入字符串函数:
void insert(char *s) {
int p = 0;
for (int i = 0; s[i]; i++) {
int j = s[i] - 'a'; // 映射26位字母到0~25
if (!ch[p][j]) ch[p][j] = ++idx; // 建立第一次在p节点遇见的字母的分支
p = ch[p][j]; // 更新p到下一层
}
cnt[p]++; // 到达字符串末尾,该节点计数加1
}
这里从根节点开始,依次处理字符串的每个字符,若对应子节点不存在就创建,最后将字符串结束位置的节点计数加 1 。
查询函数:
int query(char *s) {
int p = 0;
for (int i = 0; s[i]; i++) {
int j = s[i] - 'a';
if (!ch[p][j]) return 0; // 没有找到对应分支,说明字符串没插入过
p = ch[p][j];
}
return cnt[p]; // 返回以该字符串结尾的插入次数
}
该函数用于判断某个字符串在字典树中的出现次数是否大于 0 ,沿着字符串字符对应的分支走,若途中分支不存在则返回 0,走到末尾返回对应节点的计数。
三. 字典树在蓝桥杯 B 组 C++ 赛道的应用场景
字符串前缀匹配问题:
比如题目中给出大量单词,让你统计有多少单词以某个特定前缀开头。通过字典树,插入所有单词后,用查询函数就能快速得到结果。例如有单词 "apple""app""apply",查询 "ap" 前缀,就能通过字典树高效得出有 3 个 。
字符串去重与计数:
在插入字符串时,通过节点计数,可以统计每个字符串出现的次数,同时也能实现去重功能。比如统计一篇文章中每个单词出现的频率,就可以将单词依次插入字典树,最后看每个单词对应节点的计数 。
四、示例题目及分析:
小蓝的神秘图书馆
问题描述
小蓝是图书馆的管理员,他负责管理图书馆的所有书籍。
图书馆有 N 本书,每本书都有名字,分别为 S1,S2,...,SN。
图书馆的读者们经常来询问小蓝,他们会给小蓝一个字符串T,
希望小蓝能告诉他们,图书馆里有多少本书的名字是以 T 的前缀开头的。
小蓝需要回答他们 M次 这样的询问。
现在,小蓝需要你的帮助。你能帮助小蓝解决这个问题,从而提升图书馆的服务质量书馆的服务质量吗?
解题思路
本题可使用字典树(Trie 树)来解决,以下是具体思路:
构建字典树
- 定义字典树的数据结构:
- 用一个二维数组
son
来表示字典树的节点关系,son[i][j]
表示节点i
的字符j
对应的子节点(这里j
可通过字符减去'a'
映射到 0 - 25 的整数,用于表示 26 个英文字母)。 - 用一个数组
cnt
记录每个节点被经过的次数,即有多少字符串以该节点为前缀。 - 用一个变量
TOT
记录当前字典树中使用的节点总数。
- 用一个二维数组
- 插入操作:
- 遍历每本书的名字字符串。从字典树的根节点(编号为 0 )开始,对于字符串中的每个字符,将其转换为 0 - 25 的索引值(
u = S[i] - 'a'
)。 - 检查当前节点是否存在该字符对应的子节点,如果不存在则创建一个新节点(
son[q][u] = ++TOT
),并将当前节点移动到该子节点(q = son[q][u]
)。 - 每经过一个节点,将该节点的计数加 1(
cnt[q]++
),表示有一个字符串经过了此节点。
- 遍历每本书的名字字符串。从字典树的根节点(编号为 0 )开始,对于字符串中的每个字符,将其转换为 0 - 25 的索引值(
查询操作
- 对于每个查询字符串
T
,同样从根节点开始。 - 遍历查询字符串的每个字符,将字符转换为索引值并沿着字典树的对应分支移动。
- 如果在移动过程中遇到某个字符对应的子节点不存在(
son[q][u] == 0
),说明不存在以该查询字符串为前缀的书籍名字,直接返回当前的计数(此时计数为 0 )。 - 如果顺利遍历完查询字符串,那么此时所在节点的
cnt
值就是以该查询字符串为前缀的书籍名字的数量,返回这个值。
整体流程
- 读取书籍数量
N
和查询次数M
。 - 循环
N
次,读取每本书的名字并插入到字典树中。 - 循环
M
次,读取每个查询字符串,调用查询函数得到结果并输出
cpp
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;//N: 最大节点数的估计值
int n,q,cnt[N*27],son[N][27],TOT=0;
//n: 插入的字符串数量
//q: 查询的次数
//cnt[]: 记录每个节点被访问的次数(即有多少字符串以该节点为前缀)
//son[][]: 字典树的子节点数组,son[u][c] 表示节点 u 的字符 c 对应的子节点
//TOT: 当前使用的节点总数
//构造前缀树
void insert(string S) {
int q=0;
for(int i=0; i<S.size(); i++) {
int u=S[i]-'a';//映射,把字符转为数字索引
if(son[q][u] == 0)//检查当前节点 q 是否存在字符 u 对应的子节点。
son[q][u]=++TOT;// 创建新节点并分配唯一编号。
q=son[q][u];
cnt[q]++;
}
}
//匹配子串
int query(string T){
int q=0,ans=0;
for(int i=0;i<T.size();i++){
int u=T[i]-'a';
q=son[q][u];
if(q==0)return ans;//没找到
}
return cnt[q];
}
int main() {
cin>>n>>q;
string s;
for(int i=1; i<=n; i++) {
cin>>s,
insert(s);
}
for(int i=1; i<=q; i++) {
cin>>s,
cout<<query(s)<<'\n';
}
return 0;
}