Trie树
引言
众所周知,字典的用途无非就是用来查找字的。顾名思义,字典树自然也是起到查找的作用。我们可以看看以下几个问题:
回答 : \color{Green}回答: 回答:
利用 map 存储每个单词,调用 count 即可得出结果
回答 : \color{Green}回答: 回答:
将单词拆分后,用 map存储每一段,调用 count 得出结果
若 n <= 2e5 时,如果继续使用map,将会 TLE,这时我们就需要使用一种高级的数据结构,即 Trie树
基本概念及性质
Tire树,又称字典树。
- 是一种高效存储查找字符串集合的数据结构
- 核心思想是利用空间换时间,利用字符串的公共前缀来降低查询时间的开销
- 适用于 全是同类字符的情形:
- 小写字母
- 大写字母
- 数字
- 0 / 1
性质 \color{Orange}性质 性质
下标是 0 的节点 (头节点),即是根节点,又是空节点
树的每个节点包含的子节点字符都不相同
从根节点到某一节点,路径上所经过的字符拼接起来,就是该节点对应的字符串
实现思路及图解
int son[N][26];// 后面的 26 数字视题目而定
int idx;
// son[][]存储树中每个节点的子节点
// idx 记录每个字符的编号
核心思路 \color{Red}核心思路 核心思路:
- 从前往后依次遍历每个字符,看是否有此字母作为子节点,没有则创建新的节点
- 在每个单词结尾打上标记,表示以当前字母结尾的单词存在
插入操作
对于每个字符,我们给它指定一个插入位置,即给每个字母贴上编号
如 数组 s o n [ i ] [ j ] = k son[~i~][~j~] =k son[ i ][ j ]=k ,表示 编号为 i \color{Orange}编号为i 编号为i 的节点的, 第 j 个 \color{Orange}第j个 第j个孩子, 是 编号为 k \color{Orange}编号为k 编号为k 的节点
- 说白了就是, i , k 表示节点的位置编号,这是相对整棵树而言的 \color{red}i ~,~ k 表示节点的位置编号,这是相对整棵树而言的 i , k表示节点的位置编号,这是相对整棵树而言的
如图,当我们依次插入 abcd,abd,ace,bcd ,abda 时,我们将得到第一种编号, 蓝色 \color{blue}蓝色 蓝色表示编号结果。因为先输入的是 abcd,所以 a ,b,c,d的分别是 1,2,3,然后输入的是abd,因为 a,b 此前已经输入,即是公共前缀,故从 d 开始编号,所以 d 是 4,此后输入以此类推
可以发现相同的字母的编号可能不同
- j 表示孩子编号,表示节点 i 的第 j 个的孩子,这是相对节点 i 而言的 \color{red}j 表示孩子编号,表示节点 ~i~ 的第 ~j ~个的孩子,这是相对节点 ~i~ 而言的 j表示孩子编号,表示节点 i 的第 j 个的孩子,这是相对节点 i 而言的
如图,当我们依次插入 abcd,abd,ace,bcd ,abda 时,得到第一种编号的同时,我们可以得到第二种编号, 绿色 \color{Green}绿色 绿色表示编号结果。因为每个节点最多有26个子节点,故此我们可以按他们的字典序从0 ~ 25编号,也就是 字母 A S C L L 码 − a 的 A S C L L 码 字母ASCLL码~-~a的ASCLL码 字母ASCLL码 − a的ASCLL码。因为先输入的是 abcd,所以 a ,b,c,d的分别是 0,1,2,3,此后输入以此类推
可以发现相同字母的编号相同
代码模板
void insert(char *str)
{
int p = 0; // 从根节点出发,根节点的编号为 0
for(int i = 0; str[i]; i ++ )
{
int u = str[i] - 'a';// 第二种编号
if(!son[p][u]) // 如果之前没有从 p 到 u 的前缀
son[p][u] = ++idx; // idx 即为第一种编号
p = son[p][u]; // 顺着字典树往下走
}
}
查找操作
(1) 查找某个单词出现次数 \color{Orange}查找某个单词出现次数 查找某个单词出现次数
对于一个给定的字符串,我们从左往右扫描每个字母,顺着字典树往下找
- 若能走得到这个字母,往下走,当字符串扫完后,返回此 字符串出现的次数 c n t [ p ] \color{Red}字符串出现的次数 cnt[ ~p ~] 字符串出现的次数cnt[ p ]
- 若能走不到这个字母,结束查找,即没有这个前缀, 返回 0 \color{Red}0 0
我们可以使用
数组
c
n
t
[
]
\color{Red}数组cnt[ ~]
数组cnt[ ]记录以当前字母结尾的单词次数 ,插入完毕后,我们使 数组
c
n
t
[
p
]
cnt[~p~]
cnt[ p ]加 1,表示以当前字母结尾的单词数个数加 1
如下图所示,依次插入单词后,我们用 灰色 \color{Gray}灰色 灰色 表示以该字母结尾的单词出现的次数
代码模板为
int cnt[N]; // cnt[]存储每个节点结尾的单词结尾,即打标记
void insert(char *str)
{
int p = 0;
for(int i = 0; str[i]; i ++ )
{
int u = str[i] - 'a';
if(!son[p][u])
son[p][u] = ++idx;
p = son[p][u];
}
cnt[p] ++; // 表示当前单词数 加 1
}
int query(char *str)//查找以某个单词出现的次数
{
int p = 0;
for(int i = 0; str[i]; i ++ )
{
int u = str[i] - 'a';
if(!son[p][u]) return 0;
p = son[p][u];
}
return cnt[p];
}
例如,当我们查找 ac 这个单词出现的次数时
对于 数组 s t r [ ] 数组 str[~] 数组str[ ],有
i ~i~ i | 0 | 1 | 2 |
---|---|---|---|
s t r [ i ] str[~i~] str[ i ] | 'a’ | ‘c’ | ’\0‘ |
执行 \color{Purple}执行 执行 u = s t r [ i ] − ′ a ′ ~ u = str[~i~] - ~'a' u=str[ i ]− ′a′
有 u = ′ a ′ − ′ a ′ = 0 u = 'a' - 'a' = 0 u=′a′−′a′=0
因为此时 s o n [ p ] [ u ] = s o n [ 0 ] [ 0 ] = 1 ! = 0 son[~p~][~u~]= son[~0~][~0~]~ = 1~!= ~0 son[ p ][ u ]=son[ 0 ][ 0 ] =1 != 0
故 执行 \color{Purple}执行 执行 p = s o n [ p ] [ u ] = s o n [ 0 ] [ 0 ] = 1 ~ p = son[~p~][~u~]= son[0][0] = 1 p=son[ p ][ u ]=son[0][0]=1(根节点 的编号为 0,它的第 0 个 孩子编号为 1)
故 如下图所示,走到编号为 1 的 a 处
执行 \color{Purple}执行 执行 u = s t r [ i ] − ′ a ′ ~ u = str[~i~] - ~'a' u=str[ i ]− ′a′
有 u = ′ c ′ − ′ a ′ = 2 u = 'c' - 'a' = 2 u=′c′−′a′=2
因为此时 s o n [ p ] [ u ] = s o n [ 1 ] [ 2 ] = 6 ! = 0 son[~p~][~u~]= son[~1~][~2~]~ = 6~!= ~0 son[ p ][ u ]=son[ 1 ][ 2 ] =6 != 0
故 执行 \color{Purple}执行 执行 p = s o n [ p ] [ u ] = s o n [ 1 ] [ 2 ] = 6 ~ p = son[~p~][~u~]= son[1][2] = 6 p=son[ p ][ u ]=son[1][2]=6 (a 的编号为 1,它的第 2 个 孩子编号为 6)
故 如下图所示,走到编号为 6 的 c 处
最后 执行 \color{Purple}执行 执行 c n t [ p ] = c n t [ 6 ] = 3 ~ cnt[~p~] = ~ cnt[~6~] = 3 cnt[ p ]= cnt[ 6 ]=3,我们即可得到单词 ac 的数量为 3
(2) 查找某个前缀出现次数 \color{Orange}查找某个前缀出现次数 查找某个前缀出现次数
对于一个给定的前缀字符串,我们从左往右扫描每个字母,顺着字典树往下找
- 若能走得到这个字母,往下走,当字符串扫完后,返回此 前缀出现的次数 s u m [ p ] \color{Red}前缀出现的次数 ~sum[ ~p ~] 前缀出现的次数 sum[ p ]
- 若能走不到这个字母,结束查找,即没有这个前缀, 返回 0 \color{Red}0 0
我们可以使用 数组 s u m [ ] \color{Red}数组sum[ ~] 数组sum[ ]记录以此前缀出现的次数 ,插入过程中,我们使 数组 s u m [ s o n [ p ] [ u ] ] sum[~son[~p~][~u~]~] sum[ son[ p ][ u ] ]加 1,表示此前缀的某段部分的出现的次数加 1
依次插入单词后,我们用 灰色 \color{Gray}灰色 灰色 表示此单词出现的次数,
相应地,我们可以得到各个前缀段出现的次数,我们用 黄色 \color{Yellow}黄色 黄色 表示以该前缀段出现(从 该字母 到 根节点 所经过的字母)的次数,如下图所示
代码模板为
int sum[N];// sum[] 记录某段前缀出现的次数
void insert(char *str)
{
int p = 0;
for(int i = 0; str[i]; i ++ )
{
int u = str[i] - 'a';
if(!son[p][u])
son[p][u] = ++idx;
sum[son[p][u]] ++;//表示此前缀某段部分出现次数加 1
p = son[p][u];
}
}
int find(char *str)//查询某个前缀出现次数
{
int p = 0;
for(int i = 0; str[i]; i ++ )
{
int u = str[i] - 'a';
if(!son[p][u]) return 0;
p = son[p][u];
}
return sum[p];
}