Trie字典树
先看一道例题
给出
n
n
n个字符串,以及
m
m
m个询问。每次询问读入一个字符串,求该字符串是多少个字符串的前缀。
每个字符串长度小于
1
0
2
10^2
102,n和m小于
1
0
5
10^5
105。
【输入】
4
anan
amn
aman
anann
3
ana
ama
a
【输出】
2
1
4
朴素算法:暴力搜索,对于每个询问,把所有的n个字符串搜索一遍,统计答案。时间复杂度大于 1 0 10 10^{10} 1010。显然,这样时间是非常大的。
所以我们需要使用字典树。
什么是字典树?
字典树,顾名思义,首先它是一棵树。
而且它是用来储存字符串的!!!
它的根节点为空,其余每个节点代表一个字符。从根节点开始,沿着一定的路径,加上沿途遇到的字符,到达每个节点,都能得到对应的一个字符串。
建树
设
f
[
i
]
[
c
]
f[i][c]
f[i][c]表示字典树中编号
i
i
i节点的儿子中,表示字符
c
c
c的儿子的编号。
为了节约空间,只有建树时需要用到的节点,才会被开启,其余都是不存在的。(这句话如果不好理解可以看看操作步骤和代码)
当前我们将字符串
s
[
k
]
s[k]
s[k]加入字典树,步骤如下:
1、字符串从左往右,对应地从字典树的根节点开始往下走;
2、如果当前的
f
[
i
]
[
s
[
k
]
[
j
]
]
f[i][s[k][j]]
f[i][s[k][j]]为0,说明这个节点还未被开启,将节点总数
n
u
m
+
1
num+1
num+1,然后
f
[
i
]
[
s
[
k
]
[
j
]
]
=
n
u
m
f[i][s[k][j]]=num
f[i][s[k][j]]=num;
3、继续往下走到对应的儿子节点。
i
=
f
[
i
]
[
s
[
k
]
[
j
]
]
i=f[i][s[k][j]]
i=f[i][s[k][j]],
j
+
1
j+1
j+1。
4、当走完整个字符串时,给当前的节点
i
i
i向
k
k
k连一条边,表示字符串
s
[
k
]
s[k]
s[k]的终点在节点
i
i
i。
建树可以用递归实现,也可以用循环实现。以下贴上递归代码,循环类似。
void make(int i,int j)
{
if(j==slen)
{
add(i,k);
return;
}
else
{
if(f[i][s[k][j]]==0) f[i][s[k][j]]=++num;
dfs(f[i][s[k][j]],j+1);
}
}
for(k=1;k<=n;k++)
{
scanf("%s",s[k]");
slen=strlen(s[k]);
make(0,0);
}
效果图
如前面的样例,四个字符串用字典树储存的效果。
anan
amn
aman
anann
优势
由上图显而易见,当多个字符串有相同前缀时,相同的字符只会储存一次,节省了很大的空间。
同时,用字典树储存字符串,可以对许多关于字符串前缀的题目有很大帮助。
回到例题
显然,这题我们需要先用
n
n
n个字符串建立一棵字典树。
与此同时,记录
s
u
m
[
i
]
sum[i]
sum[i]表示
n
n
n个字符串中前缀为
s
t
r
i
n
g
[
i
]
string[i]
string[i]的个数,
s
t
r
i
n
g
[
i
]
string[i]
string[i]为字典树中到节点
i
i
i处所表示的字符串(此处只是为了方便说明,在实现时这个
s
t
r
i
n
g
string
string数组并不存在)。每到一个节点
i
i
i,就给
s
u
m
[
i
]
+
1
sum[i]+1
sum[i]+1。
如何处理询问?
将询问串放入字典树中,类似建树的方式往下走。如果对应的节点不存在,则直接返回并输出
0
0
0.
否则一直走到该串的末尾,对应的
s
u
m
[
i
]
sum[i]
sum[i]即为答案。
结束语
此时,相信你已经学会了。字典树模板都是一样的,对于不同的题目不同的要求,都以模板为基础,再按需添加各种操作,难题便迎刃而解。
面对更多的困难与挑战,需要你更多思考、灵活变通,一切都不是问题!