蓝桥杯b组c++赛道---字典树
一. 字典树(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~25if (!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
次,读取每个查询字符串,调用查询函数得到结果并输出
#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;
}