先看一道例题
给出n个字符串,以及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个字符串搜索一遍,统计答案。时间复杂度大于10^10。显然,这样时间是非常大的。
如果要优化时间的话,我们就可以使用字典树。
字典树,又叫做Trie树,顾名思义,首先它必须得是一棵树。
而且它是用来储存字符串的!!! 就像一本字典一样,里面什么都有(只要你存了)
它的根节点为空,其余每个节点代表一个字符。从根节点开始,沿着一定的路径,加上沿途遇到的字符,到达每个节点,都能得到对应的一个字符串。
字典树的作用
1、可以判断一个字符串之前是否出现过。
2、可以判断当前这个字符串是多少个字符串的前缀。
3、等等等等…
其实大概就这些了,具体应用就要靠读者自己去思考了。
字典树的性质
1、沿着字典树一直走,如果经过一个被标记的点,那么从根节点到当前这个节点所经过的所有字符组成的字符串一定出现过,也就是说如果一个没有出现过的字符串在字典树上是走不出来的。
2、不论沿着字典树怎么走,都不会走到两个被标记的不同的点,使得它们沿途经过的点是完全相同的,意思就是说相同的字符串在字典树里面只会出现一次。
设trie[i].to[c]表示字典树中编号i节点的儿子中,字符c的儿子的编号。
设trie[i].bz表示当前这个节点是否是一个字符串的结尾(1是0不是)。
设tot表示当前Trie树里节点的总数(也可以看做编号),当我们要加入一个点的时候就将编号+1。
一开始字典树是一棵空树,不,实际上不是,因为它有一个为空的根节点。
为了节约空间,只有需要用到的节点,才会被开启,其余都是不存在的。(这句话如果不好理解可以看看操作步骤和代码)
设当前我们将字符串s加入字典树,步骤如下:
1、先将当前的节点赋成根节点,之后我们从左到右扫s这个字符串(注意字典树的根节点为空);
2、如果当前的点后面没有一个s[i]的字符,那就在当前这个点的下面加上s[i]这个字符的点;
3、然后沿着字典树往s[i]这个字符走下去。
4、当字符串中所有的字符都加入完了这棵字典树时,要将这个字符串的最后一个字符的编号打上标记,表示当前这个节点是一个字符串的结尾。
Code:
int tot=0;//tot一定要是根节点的编号,因为当根节点往下走的时候tot+1不能和根节点的编号一样
void insert(char s[])
{
int t=0;//根节点的编号为0
for (int i=1;i<=len;i++)//循环枚举s的每一位
{
int k=s[i]-'a'+1;//将字母转换成数字
if (trie[t].to[k]==0) trie[t].to[k]=++tot;//如果当前的节点下面没有k这个数字(字符)就在当前节点下加入k这个点,同时将它编号为tot;如果当前节点已经有了k这个数字(字符)就不用再加
t=trie[t].to[k];//之后再沿着字典树走下去
}
trie[t].bz=true;//表示当前节点是一个字符串的结尾
}
for(int i=1;i<=n;i++)
{
scanf("%s",s[i]);
len=strlen(s[i]);
insert(s[i]);
}
效果图:
如前面的样例,四个字符串用字典树储存的效果。
anan
amn
aman
anann
同时,图中被红线画出来的点都被打了标记;根节点的编号为0,从根节点往下的第一个a的编号是1,以此类推,编号为1的a下面的n编号为2,编号为2的n下面的a编号为3,编号为3的a下面的n编号为4。编号为1的a下面的m编号为5,编号为5的m下面的n编号为6。编号为5的m下面的a编号为7,编号为7的a下面的n编号为8。最后编号为4的n下面的n编号为9。
如果我们要查询一个字符串是否在字典树中出现该怎么办呢?
1、首先,我们按照这个字符串中的每个字符在字典树中走下去。
2、如果有一个字符没有出现在字典树中,那么这个字符串就没有出现,直接退出。
3、如果扫完了这个字符串,也走完了这棵字典树,那么我们就判断一下当前这个节点是否有标记,有标记就说明这个字符串出现过。
Code:
int tot=0;//根节点编号为0
int find()
{
scanf("%s",s+1);//判断s是否出现过
int len=strlen(s+1);//s的长度为len
int t=0;//当前节点为根节点
for (int i=1;i<=len;i++)
{
int k=s[i]-'a'+1;//将字符转换为数字
t=tree[t].to[k];//沿着字典树走下去
if (t==0) break;//如果字典树中没有这个节点就说明这个字符串没有出现,退出
}
return tree[t].bz;//如果当前编号t是一个字符串的结尾,就说明这个字符串出现过,否则就没有出现。
}
由上图显而易见,当多个字符串有相同前缀时,相同的字符只会储存一次,节省了很大的空间。
同时,用字典树储存字符串,可以对许多关于字符串前缀的题目有很大帮助。
显然,这题我们需要先用n个字符串建立一棵字典树。
与此同时,记录tree[t].bz表示n个字符串中前缀为string[t]的个数,string[t]为字典树中到节点i处所表示的字符串(此处只是为了方便说明,在实现时这个string数组并不存在)。每到一个节点t,就给tree[t].bz+1。
如何处理询问?
将询问串放入字典树中,类似建树的方式往下走。如果对应的节点不存在,则直接返回并输出0。
否则一直走到该串的末尾,对应的tree[i].bz即为答案。
Code:
#include
#include
#include
#include
#include
using namespace std;
int n,m,len,tot=0;
struct tree{
int to[27],bz;
}trie[100000];
char s[1000][1000];
void insert(char s[])
{
int t=0;
for (int i=1;i<=len;i++)
{
int k=s[i]-'a'+1;
if (trie[t].to[k]==0) trie[t].to[k]=++tot;
t=trie[t].to[k];
trie[t].bz++;
}
}
int find(char s[])
{
int t=0;
for(int i=1;i<=len;i++)
{
int k=s[i]-'a'+1;
t=trie[t].to[k];
if(t==0) break;
}
return trie[t].bz;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%s",s[i]+1);
len=strlen(s[i]+1);
insert(s[i]);
}
scanf("%d",&m);
for(int i=1;i<=m;i++)
{
scanf("%s",s[i]+1);
len=strlen(s[i]+1);
printf("%d\n",find(s[i]));
}
return 0;
}
此时,相信你已经学会了。字典树模板都是一样的,对于不同的题目不同的要求,都以模板为基础,再按需添加各种操作,难题便迎刃而解。
面对更多的困难与挑战,需要你更多思考、灵活变通,一切都不是问题!
练习:
1.1099. 寻找字符串
2.5795. 【2018提高组】模拟A组&省选 词典
作者:zsjzliziyang
QQ:1634151125
转载及修改请注明
本文地址:https://blog.csdn.net/zsjzliziyang/article/details/81869305