注:
- 前言是废话
- 线段树不分左右,说左右是为了好理解
- 实操指实际操作
- qwq 指根
理论
前言
Trie字典树 是一个树(
定义
Trie字典树 指的是:某个字符串集合构造的有根树。
好处
由于 Trie字典树 较好的利用了字符串的公共前缀,因此有效的节约存储空间。
查询效率比哈希树高。
用途
用于统计、排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。
性质
- 根结点不包含字符,除根结点外每一个结点都只包含一个字符;
- 从根结点到某一结点,路径上经过的字符连接起来,为该结点对应的字符串;
- 每个结点的所有子结点包含的字符都不相同。
正片
input:
abc
ghi
abcdef
abf
构建
先处理 abc
。
第一个进来 a,发现 qwq 左侧和右侧都没有 a,于是将 a 弄到左边。
依次类推,将 b 和 c 也弄进来。
然后我们发现 c 是这个字符串结尾,于是标记一下。
然后处理 ghi
,因为 g 没出现过,所以这个字符串丢去右边。
接着是 abcdef
,前面 abc 刚刚好都有,所以只用从 c 往下建 def
。
最后是 abf
,ab 已经有了,直接在 b 的旁边构建一个 f 就刑。
题外话:没错,仅有小写字母是二十六叉树。
所以说,我们的数组大概是这样子的:
但是可以发现,如果再来一个 abc
,它会和原来的重合,所以我们再用一个数组来标记以某个节点为结束的单词有几个。
另:第 i 个处理的字母我们将其标记为 i 号节点。
这个数组怎么看呢,很简单。
0 0 0 指根, a 0 , i a_{0,i} a0,i 就是指根的子节点。
也就是说, a i , j a_{i,j} ai,j 就是指 i号节点 的子节点。
题外话:没错,这棵树不区分左右子节点。
所以说,如果我们用邻接矩阵储存仅有小写字母的字典树,数组大概要开 max { s i . s i z e } × 26 \max\{s_i.size\} \times 26 max{si.size}×26。
构建实操
例题:东方博宜2381。
首先我们定义一个 p p p 为当前搜索的根,从 0 0 0 也就是根开始找起。
void insert(char s[]){
int p = 0;
}
然后循环 s s s 数组。
void insert(char s[]){
int p = 0;
//for(int i = 0; s[i] != '\0'; i++)
for(int i = 0; s[i]; i++){
}
}
如果查找当前根找不到,就添加这个点。
void insert(char s[]){
int p = 0;
//for(int i = 0; s[i] != '\0'; i++)
for(int i = 0; s[i]; i++){
int x = s[i]-'a';
if(!t[p][x]) t[p][x] = ++ind;
}
}
然后更新 p p p,而且无论是否新建点(无论 s i s_i si 是否是当前节点 p p p 的子节点), p p p 都应该更新到 t p , x t_{p,x} tp,x。
还有因为最后结束循环时还会更新一次,所以说我们要标记结束点时刚刚好就可以用 p p p。
void insert(char s[]){
int p = 0;
//for(int i = 0; s[i] != '\0'; i++)
for(int i = 0; s[i]; i++){
int x = s[i]-'a';
if(!t[p][x]) t[p][x] = ++ind;
p = t[p][x];
}
cnt[p]++;
}
查询实操
查询大致一样,只是如果当前根没有,直接 return
。
这道题如果查找了找不到的直接返回
0
0
0(没有投票,票数为
0
0
0),所以说找不到就 return 0
。
刚刚好,如果查询完改数组最后一个不是结束点,直接返回 c n t p cnt_p cntp,刚刚好就是 0 0 0。
void insert(char s[]){
int p = 0;
for(int i = 0; s[i]; i++){
int x = s[i]-'a';
if(!t[p][x]) return 0;
p = t[p][x];
}
return cnt[p];
}
练习讲解
例题一:洛谷P8306
这个题和和东方博宜的板子很像,但是难一些。
首先我们的数组要扩到 65 65 65,然后要写个函数处理字符串里的字母。
因为只有大小写字母和数字,所以 if
判断然后返回就行。
但是直接减去 A
、a
、0
并不可行,所以我们应该在储存时加一个数,使大小写字母和数字的存储错开。
int get(char x){
if(x >= 'A' && x <= 'Z')
return x-'A'; //大写字母
else if(x >= 'a' && x <= 'z')
return x-'a'+26; //小写字母
else return x-'0'+52; //数字
}
另外,查询时,因为是问的前缀,所以我们 for
储存时一边循环一边
c
n
t
p
cnt_p
cntp++。
void insert(char s[]){
int p = 0;
for(int i = 0; s[i]; i++){
int x = get(s[i]);
if(!t[p][x]) t[p][x] = ++ind;
p = t[p][x];
cnt[p]++;
}
}
值得一提的题外话:警钟长鸣。
例题二:洛谷P2580
值得一提的是,这比洛谷的板子要简单,只是在查询的时候需要更改一下。
- 每次查询,如果在循环查找名称时,没有找到,直接输出
WRONG
并且 return。 - 每次查询结束,先判断是否存在这个名字,如果并没有(
c
n
t
p
=
0
cnt_p = 0
cntp=0),就输出
WRONG
。(这就是 hack 数据) - 如果存在该名字,将
c
n
t
cnt
cnt 数组这个地方修改为
−
1
-1
−1,并输出
OK
。 - 如果
c
n
t
cnt
cnt 数组这个地方为
−
1
-1
−1,输出
REPEAT
。
void query(string s){
int p = 0;
for(int i = 0; s[i]; i++){
int x = s[i]-'a';
if(!t[p][x]){
cout << "WRONG" << endl;
return ;
}
p = t[p][x];
}
if(!cnt[p])
cout << "WRONG" << endl;
else if(cnt[p] == -1)
cout << "REPEAT" << endl;
else cout << "OK" << endl,cnt[p] = -1;
}
是吧,和上面的第一题很像。
O v e r Over Over