又称单词查找树,trie树,是一种树形结构,是一种哈希树的变种。典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高。
(以上内容摘自百度百科)
trie树是一种ennnnnnn树,2333333,本弱的表达能力真的是很弱额。那trie树是用来干什么的那?做题的trie树把字符存到节点里,如上所说,他利用字符串的公共前缀节省查询时间。这也说明了trie树的存储方式是对于不同字符串的相同前缀,他们共用一些相同的节点。比如:abcd和abdd,在存abcd的时候发现0号节点(trie树的根节点,用于把首字母就不同的字符串连接起来)没有a这个儿子,就新建一个a,依次往下建,建出一条链,a->b->c->d,然后在建abdd的时候发现0号节点有a这个儿子了,就不在建了,而是 直接跳到a那里,在往下发现a又有b这个儿子了,还是直接跳过去,然后发现b没有d这个儿子,所以新建一个d,然后d又建一个儿子是d。这就是trie树的建树机制啦。上面的例子建出来的树长下面的样子
而具体实现就是用数字来代表字符,开一个数组或者把所有都封装到一个结构体里来表示trie树就可以了。
来看一道模板题https://www.luogu.org/problemnew/show/P1481
风之子刚走进他的考场,就……
花花:当当当当~~偶是魅力女皇——花花!!^^(华丽出场,礼炮,鲜花)
风之子:我呕……(杀死人的眼神)快说题目!否则……-_-###
花花:……咦好冷我们现在要解决的是魔族的密码问题(自我陶醉:搞不好魔族里面还会有人用密码给我和菜虫写情书咧,哦活活,当然是给我的比较多拉*^_^*)。魔族现在使用一种新型的密码系统。每一个密码都是一个给定的仅包含小写字母的英文单词表,每个单词至少包含1个字母,至多75个字母。如果在一个由一个词或多个词组成的表中,除了最后一个以外,每个单词都被其后的一个单词所包含,即前一个单词是后一个单词的前缀,则称词表为一个词链。例如下面单词组成了一个词链:
i int integer
但下面的单词不组成词链:
integer
intern 现在你要做的就是在一个给定的单词表中取出一些词,组成最长的词链,就是包含单词数最多的词链。将它的单词数统计出来,就得到密码了。
风之子:密码就是最长词链所包括的单词数阿……
花花:活活活,还有,看你长得还不错,给你一个样例吧:
输入格式:
这些文件的格式是,第一行为单词表中的单词数N(1<=N<=2000),下面每一行有一个单词,按字典顺序排列,中间也没有重复的单词咧!!
输出格式:
你要提交的文件中只要在第一行输出密码就行啦^^
很显然,答案就是包含单词最多的单词包含了多少单词,并且除它本身外 ,其他单词都得是它的前缀才算被他包含。一眼想到trie树嘛,每个单词插入trie树,在结束的位置记录一下这个位置的点是一个结束位置,但是因为要求的是包含其他单词最多的单词包含了多少单词,所以不只是要在这里记录一下,从这个位置往下的每一个节点都要加一个单词,
就是这里以1节点结尾的单词,包含在以2,3,4结尾的单词里。这个在建完树后加一个dfs处理一遍就好了。
代码:
1 #include<iostream> 2 #include<cstring> 3 #include<cstdio> 4 using namespace std; 5 int tree[2005][2005]; 6 int tot; 7 int n; 8 int ans; 9 int end[2005];//记录以被以节点i结尾的单词包含的单词有多少个,当然如果没有以节点i为结尾的单词也还是要记录的,因为下面的节点需要用,所以最后统计的时候只统计叶子节点(也就是最长的那些单词就好了),但是由于是取max,都统计一遍也不会出错 10 string s; 11 void insert()//插入单词 12 { 13 int len=s.length(); 14 int now=0; 15 for(int i=0;i<len;++i)//从根节点开始 16 { 17 int p=s[i]; 18 if(!tree[now][p])tree[now][p]=++tot;//如果没有这个节点,就新建一个 19 now=tree[now][p];//此时肯定是有这个儿子节点的,所以直接跳过去 20 } 21 end[now]++;//以now为结尾的单词数+1 22 } 23 void dfs(int x) 24 { 25 for(int i=0;i<=2000;++i) 26 if(tree[x][i]) 27 { 28 end[tree[x][i]]+=end[x]; 29 dfs(tree[x][i]); 30 } 31 } 32 int main() 33 { 34 scanf("%d",&n); 35 for(int i=1;i<=n;++i) 36 { 37 cin>>s; 38 insert(); 39 } 40 dfs(0); 41 for(int i=0;i<=2000;++i)ans=max(ans,end[i]); 42 printf("%d",ans); 43 return 0; 44 }
又一道水水的例题https://www.luogu.org/problemnew/show/P2580
这道题是比上面那道题简单的,所以过了上面那道的话,写这道题应该很轻松
代码:
1 //建trie树 2 #include<iostream> 3 #include<cstdio> 4 using namespace std; 5 int tree[1000005][28]; 6 int n; 7 int m; 8 string tmp; 9 int len; 10 int cnt; 11 bool end[10000005]; 12 bool vis[10000005]; 13 void Insert() 14 { 15 int now=0; 16 for(int i=0;i<len;++i) 17 { 18 int p=tmp[i]-'a'; 19 if(tree[now][p]==0)tree[now][p]=++cnt; 20 now=tree[now][p]; 21 } 22 end[now]=1; 23 } 24 int ok() 25 { 26 int now=0; 27 for(int i=0;i<len;++i) 28 { 29 int p=tmp[i]-'a'; 30 if(tree[now][p]==0)return 0; 31 now=tree[now][p]; 32 } 33 if(!end[now])return 0; 34 if(!vis[now]) 35 { 36 vis[now]=1; 37 return 1; 38 } 39 return 2; 40 } 41 int main() 42 { 43 scanf("%d",&n); 44 for(int i=1;i<=n;++i) 45 { 46 cin>>tmp; 47 len=tmp.length(); 48 Insert(); 49 } 50 scanf("%d",&m); 51 while(m--) 52 { 53 cin>>tmp; 54 len=tmp.length(); 55 int flag=ok(); 56 if(flag==1)printf("OK "); 57 if(flag==2)printf("REPEAT "); 58 if(!flag)printf("WRONG "); 59 } 60 return 0; 61 }
例题3https://www.luogu.org/problemnew/show/P2922
贝茜正在领导奶牛们逃跑.为了联络,奶牛们互相发送秘密信息.
信息是二进制的,共有M(1≤M≤50000)条.反间谍能力很强的约翰已经部分拦截了这些信息,知道了第i条二进制信息的前bi(l《bi≤10000)位.他同时知道,奶牛使用N(1≤N≤50000)条密码.但是,他仅仅了解第J条密码的前cj(1≤cj≤10000)位.
对于每条密码J,他想知道有多少截得的信息能够和它匹配.也就是说,有多少信息和这条密码有着相同的前缀.当然,这个前缀长度必须等于密码和那条信息长度的较小者.
在输入文件中,位的总数(即∑Bi+∑Ci)不会超过500000.
题目意思就是说:有n个已知密码串和m个拦截密码串,只有当一个已知密码串是一个拦截密码串的前缀或者一个拦截密码串是一个已知密码串的子串的时候这个拦截密码串就算是找到了一个匹配,问每一个拦截密码串的匹配数。
很显然还是用trie树来做,但这个的维护有一点复杂(也可能是因为窝太弱了)。先把已知串建trie树,然后用拦截串去匹配已知串,有一个比较易错的地方是:如果当前的拦截串在trie树上没有被匹配完,那么就代表没有以它为前缀的已知密码,所以直接输出它的那些前缀已知串的个数就行了,但如果把它匹配完了,就就还有一些以它为前缀的已知密码,所以要把那些密码的个数加上。
代码:
1 //建在结构体里的trie树 2 #include<iostream> 3 #include<cstdio> 4 #include<cstring> 5 using namespace std; 6 struct node{ 7 int sum,end; 8 int nex[2]; 9 }t[500001];//因为要维护好几个东西,所以直接扔结构体里 10 int n,m; 11 int a[500001]; 12 int len; 13 int tot; 14 int main() 15 { 16 memset(t,0,sizeof(t)); 17 scanf("%d%d",&n,&m); 18 for(int i=1;i<=n;++i) 19 { 20 scanf("%d",&len); 21 for(int j=1;j<=len;++j) 22 scanf("%d",&a[j]); 23 int now=0; 24 for(int j=1;j<=len;++j) 25 { 26 if(!t[now].nex[a[j]])t[now].nex[a[j]]=++tot; 27 now=t[now].nex[a[j]]; 28 t[now].sum++;//sum维护的就是有多少个密码用过这个点 29 } 30 t[now].end++;//建树 31 } 32 33 for(int i=1;i<=m;++i) 34 { 35 scanf("%d",&len); 36 for(int j=1;j<=len;++j)scanf("%d",&a[j]); 37 int now=0,ans=0; 38 bool flag=1;//flag记录当前串是不是被匹配完了 39 for(int j=1;j<=len;++j) 40 { 41 if(t[now].nex[a[j]])now=t[now].nex[a[j]]; 42 else{ 43 flag=0; 44 break; 45 } 46 ans+=t[now].end; 47 } 48 if(!flag)printf("%d ",ans); 49 else printf("%d ",ans+t[now].sum-t[now].end);//sum是包含end的,所以为了避免重复而减去 50 } 51 return 0; 52 }
一些01异或的题也经常用trie树来做,不过本弱找不到例题了,以后遇见了在写吧。