目录
概念
字典树,顾名思义,是关于“字典”的一棵树。即:它是对于字典的一种存储方式,所以是一种数据结构,而不是算法。这个词典中的每个“单词”就是从根节点出发一直到某一个目标节点的路径,路径中每条边的字母连起来就是一个单词。
常用来存储和查询字符串。假定接下来提到的字符串均由小写字母构成,那么Trie将是一棵 26 叉树。
Trie树:某个字符串集合对应的有根树
树的每条边:对应有恰好一个字符
树的每个顶点:代表从根到该节点的路径所对应的字符串(将所有经过的边上的字符按顺序连接起来)
初始化(initialize)
树仅包含一个根节点,该点的字符指针均指向空。
插入(insert)
首先,记录trie树的方法为:编号为 u 的节点指向了编号为 v 的节点,边对应了字母 c ,记为son[u][c] = v
当需要插入一个字符串 s时,我们令一个指针 p起初指向根节点。
然后,依次扫描 s中的每个字符 c:
若 c的指向一个己经存在的节点 q,则令 p=q
若 c的指向空,即son[0][c] == 0 成立,说明Trie中不含字符 c,则令 son[0][c] = idx,其中 idx 为新建立的节点的编号
当 s中的字符扫描完毕时,在当前指针 p上标记它是一个字符串的末尾。
为什么说要给每个“字符串”的结尾打上标记呢?
假想这样一种情况,有两个字符串:ab
,abcd。
显然,后者在Trie中形成的路径包含前者,所以在存储这两个字符串的时候仅会得到一条路径,那如何说明我们存储了两个字符串呢?很简单,只需在每个字符串的末尾打上标记,如下图所示:
对应到代码中就是再开一个 cnt
数组(全0初始化),并令 cnt[2] = 1, cnt[4] = 1
即可。
如果同一个字符串存储了多次,只需在每次存储的时候执行 cnt[p]++
即可,其中 p
是字符串末尾所对应的节点的编号。
代码如下:
const int N = 1e5 + 10;
int son[N][26], cnt[N], idx;
void insert(const string &s)
{
int p = 0;
for (int i = 0; i < s.size(); i++)
{
int c = s[i] - 'a';
if (!son[p][c]) son[p][c] = ++idx;
p = son[p][c]; //继续到下一个点
}
cnt[p]++;
}
查询(query)
查询的思路和插入相似。
当需要查询一个字符串 s时,我们令一个指针 p起初指向根节点。
然后,依次扫描 s中的每个字符 c:
若 c的字符指针指向一个己经存在的节点 q,则令 p=q
若 c的字符指针指向空,说明s不在树中,查询结束
当 s中的字符扫描完毕时,在当前指针 p上标记它是一个字符串的末尾。
int query(const string &s) {
int p = 0;
for (int i = 0; i < s.size(); i++) {
int c = s[i] - 'a';
if (!son[p][c]) return 0;
p = son[p][c];
}
return cnt[p]; // 返回字符串的个数
}
例题与样码
这道题目是字典树模板的略微改动,我们发现这道题目和一般字典树的查询不一样,字典树一般查询是看这个字符串是否出现,而这道题目这是统计这个字符串出现的次数。
#include <bits/stdc++.h>
using namespace std;
const int N=1e6+10;
char str[N];
int trie[N][26];
int n,m,t;
int End[N],tot=1;
void insert(char a[])
{
int len=strlen(a),p=1;
for(int i=0;i<len;++i)
{
int ch=a[i]-'a';
if (trie[p][ch]==0)
trie[p][ch]=++tot;
p=trie[p][ch];
}
End[p]++;//统计个数
}
int Search(char a[])
{
int len=strlen(a),p=1,ans=0;
for(int i=0;i<len;++i)
{
p=trie[p][a[i]-'a'];
if (p==0)
return ans;
ans+=End[p];
}
return ans;
}
int main()
{
scanf("%d%d\n",&n,&t);
for(int i=0;i<n;++i)
{
scanf("%s\n",str);
insert(str);
}
while(t--)
{
scanf("%s\n",str);
printf("%d\n",Search(str));
}
return 0;
}
Trie不仅可以存储字符串,还能存储整数。在存储字符串时,Trie中的每条边对应了一个字符,在存储整数时,Trie中的每条边非 0 即 1,即Trie存储的是整数的二进制表示,此时Trie是一棵二叉树。
/*首先,把所有数的二进制插入到trie树里
然后,对于每一个i,将第i个数在trie里查询
优先向不同的数字dfs
注:二进制位数最大就是31
*/
#include<bits/stdc++.h>
using namespace std;
const int N=100005;
int a[N];
int trie[N*31][3];
int n,tot;
int read()
{
char c=getchar();
int f=1,x=0;
for(;!isdigit(c);c=getchar()) if(c=='-') f=-1;
for(;isdigit(c);c=getchar()) x=x*10+c-48;
return x*f;
}
void insert(int x)
{
int p=0;
for(int i=30;i>=0;--i)
{
int c=(x>>i)&1;
if(!trie[p][c]) trie[p][c]=++tot;
p=trie[p][c];
}
}
int query(int x)
{
int p=0;
int res=0;
for(int i=30;i>=0;--i)
{
int c=(x>>i)&1;
if(trie[p][!c])
{
res+=1<<i;
p=trie[p][!c];
}
else
{
p=trie[p][c];
}
}
return res;
}
int main()
{
n=read();
for(int i=1;i<=n;++i)
{
a[i]=read();
insert(a[i]);
}
int ans=0;
for(int i=1;i<=n;++i)
{
ans=max(ans,query(a[i]));
}
cout<<ans<<endl;
return 0;
}