一.什么是Trie树?
Trie(字典树) 是一种用于实现 字符串快速检索 的多叉树结构。典型的应用是用于 统计,排序和存储大量字符串(但不是只运用于字符串,如01Trie树) 。优点是:利用字符串的公共前缀来 减少查询时间和存储空间。 能够最大限度的减少不必要的字符比较。
二.Trie树的结构
Trie树长什么样子呢,我们通过下图来说明:
可以看到Trie树的每条边对应一个字母,从根节点到其余每个节点的路径字母链接起来,就构成了一个字符串。 所以我们可以通过标记每个字符串所对应的路径的最后一个节点,就可以实现对字符串的 存储,反之,我们检验每个字符串所对应的路径是否存在且最后一个节点是否被标记,就可以实现对字符串的 检索。
三.Trie树的基本操作过程
Trie树的每个节点都拥有若干个字符指针,若是在插入或检索字符串时扫描到一个字符串c,就沿着当前节点的c字符指针,走向该指针指向的节点。
初始化
一棵空的Trie树仅包含一个根节点,该点的字符指针均指向空.
插入
当需要插入一个字符串S时,我们令一个指针P起初指向根节点,然后依次扫描S中的每个字符c:
1.若P的c字符指针指向一个已经存在的节点Q,令P=Q。
2.若P的c字符指针指向空,则新建一个节点Q,令P的c字符指针指向Q,然后令P=Q。
当S中的字符扫描完毕时,在当前节点P上标记它是一个字符串的末尾。
检索
当需要检索一个字符串S在Trie树中是否存在时,我们令一个指针P起初指向根节点,然后依次扫描S中的每个字符c:
1.若P的c字符指针指向空,则说明S没有被插入过Trie树,结束检索。
2.若P的c字符指针指向一个已经存在的节点Q,则令P=Q。
当S中的字符扫描完毕时,若当前节点P已被标记。则说明S在Trie树中存在,否则S没有被插如果Trie树。
四.代码实现
//插入
int trie[SIZE][26/*字符集的大小*/],tot=1;//初始化,tot代表当前节点的数量,SIZE是节点的最大个数(一般设为所有字符串的字符总数),假设字符串都是由26个小写字母构成
bool end[SIZE];//标记每个节点是否作为末尾
void Insert(char *str){
int p=1,n=strlen(str);
for(int i=0;i<n;i++){
int ch=str[i]-'a';
if(trie[p][ch]==0/*表示当前节点p的ch指针指向空*/)
trie[p][ch]=++tot;//新建节点
p=trie[p][ch];//更新当前节点
}
end[p]=1;
}
//检索
int trie[SIZE][26/*字符集的大小*/],tot=1;//初始化,tot代表当前节点的数量,SIZE是节点的最大个数(一般设为所有字符串的字符总数),假设字符串都是由26个小写字母构成
bool end[SIZE];//标记每个节点是否作为末尾
bool search(char *str){//检索字符串str是否存在
int p=1,n=strlen(str);
for(int i=0;i<n;i++){
int ch=str[i]-'a';
if(trie[p][ch]==0/*表示当前节点p的ch指针指向空*/)
return 0;
p=trie[p][ch];//更新当前节点
}
return end[p];
}
五. 01 Trie树
01Trie树是Trie树的经典应用,能够有效的解决点,或区间的异或最大值,最小值。
基本思想:
将整型的数字转化为二进制01字符串(高位补0),并存储在Trie树中,求两数和最大,最小异或值时,枚举每一个二进制字符串,并且从Trie树中找到每一位的最优匹配,从而得出每一个数的最大或最小异或值。将这些结果取最优即可。
时间复杂度O(32*N)= O(N)
代码实现
int trie[SIZE][2/*字符集的大小*/],tot=1;//初始化,tot代表当前节点的数量,SIZE是节点的最大个数(一般设为32*数的个数)
bool end[SIZE];//标记每个节点是否作为末尾
void Insert(int x){//在Trie树中插入数x
int p=1;
for(int i=32;i>=0;i--){
int num=(x>>i)&1;
if(trie[p][num]==0) trie[p][num]=++tot;
p=trie[p][num];
}
}
int trie[SIZE][2/*字符集的大小*/],tot=1;//初始化,tot代表当前节点的数量,SIZE是节点的最大个数(一般设为32*数的个数)
bool end[SIZE];//标记每个节点是否作为末尾
int search(int x){//查找每一个数与其他数异或所能产生的最大值
int p=1,res=0;
for(int i=32;i>=0;i--){
int num=(x>>i)&1,k=num^1/*要求异或最大值,每一位都最好不同*/;
if(trie[p][k]!=0) p=trie[p][k],res=res*2+1;//能找到
else p=trie[p][num],res=res*2;//找不到
}
return res;
}
例题分析
①前缀统计
题面
分析:
把N个字符串,插入Trie树,end[]数组记录以每个节点为结尾的字符串数量。对于每次询问,在Trie树中检索字符串T,并将答案累加每个路径节点end值即可。
SOLUTION:
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int tot=1,n,m;
int trie[N][26],end[N];
char ch[N];
void Insert(char *str){
int len=strlen(str);
int p=1;
for(int i=0;i<len;i++){
int ch=str[i]-'a';
if(trie[p][ch]==0) trie[p][ch]=++tot;
p=trie[p][ch];
}
end[p]++;
}
int search(char *str){
int len=strlen(str);
int p=1,sum=0;
for(int i=0;i<len;i++){
int ch=str[i]-'a';
if(trie[p][ch]==0) return sum;
else p=trie[p][ch],sum+=end[p];
}
return sum;
}
int main(){
freopen("test.in","r",stdin);
freopen("test.out","w",stdout);
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%s",ch);
Insert(ch);
}
for(int i=1;i<=m;i++){
scanf("%s",ch);
printf("%d\n",search(ch));
}
return 0;
}
②The XOR Largest Pair
题面
分析:
01Trie树板子题。
SOLUTION:
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int trie[32*N][2],n,num[N],tot=1,res;
bool end[N];
void Insert(int x){//将x转换为0,1串并插入
int p=1;
for(int i=30;i>=0;i--){
int k=(x>>i)&1;
if(trie[p][k]==0) trie[p][k]=++tot;
p=trie[p][k];
}
}
int search(int x){
int p=1,res=0;
for(int i=30;i>=0;i--){
int a=(x>>i)&1,b=a^1;
if(trie[p][b]!=0) res=res*2+1,p=trie[p][b];
else res=res*2,p=trie[p][a];
}
return res;
}
int main(){
freopen("test.in","r",stdin);
freopen("test.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&num[i]);
Insert(num[i]);
}
for(int i=1;i<=n;i++)
res=max(res,search(num[i]));
printf("%d\n",res);
return 0;
}
③The xor-longest Path
题面
分析:
设XOR(X,Y)表示从X点到Y点的路径异或值,由异或的运算律可得:起点为S,终点为E的一条路径,它的路径异或值就等于XOR(根节点,S)xor XOR(根节点,E)。原因也在于与0异或具有保持性。 XOR(S,E) = 0 xor XOR(S,E) =(XOR(根节点,S) xor XOR(根节点,S))xor XOR(S,E)= XOR(根节点,S)xor (XOR(根节点,S) xor XOR(S,E)) = XOR(根节点,S)xor XOR(根节点,E)。其实就是把根节点到S的这一段路径异或值抵消掉了。知道了这些,我们可以用dfs深度优先遍历遍历预处理出来根节点到每个节点的路径异或值D[1N]**,结果就是**从D[1N]中选择两个数的最大异或值,也就转化为了上一个问题。
时间复杂度:O(n + 32*n)
SOLUTION:
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
struct edge{//邻接表建图
int v,w,last;
}E[2*N];
int s,tot,n,head[N],u,v,w;
int Xor[N],res;//Xor[i]表示以从根节点到i的所有路径边的异或值 这时u->v的路径异或值==Xor[u]^Xor[v]
int trie[32*N][2],T=1;//01trie树
bool end[32*N],f[N];
void add(int u,int v,int w){
E[++tot].v=v;
E[tot].w=w;
E[tot].last=head[u];
head[u]=tot;
}
void dfs(int now){
f[now]=1;
for(int i=head[now];i!=-1;i=E[i].last){
int v=E[i].v,w=E[i].w;
if(!f[v]/*没有遍历这个点*/){
Xor[v]=Xor[now]^w;//计算异或值
dfs(v);//接着深搜
}
}
return ;
}
void Insert(int x){
int p=1;
for(int i=30;i>=0;i--){
int k=(x>>i)&1;
if(trie[p][k]==0) trie[p][k]=++T;
p=trie[p][k];
}
end[p]=1;
}
int search(int x){
int p=1,res=0;
for(int i=30;i>=0;i--){
int a=(x>>i)&1,b=a^1;
if(trie[p][b]!=0) res=res*2+1,p=trie[p][b];
else res=res*2,p=trie[p][a];
}
return res;
}
int main(){
freopen("pxor.in","r",stdin);
freopen("pxor.out","w",stdout);
scanf("%d",&n);
memset(head,-1,sizeof(head));
for(int i=1;i<n;i++){
scanf("%d%d%d",&u,&v,&w);
add(u,v,w);//必须建立双向边 这样能保证从根节点出发能到达每个节点
add(v,u,w);
}
Xor[1]=0;
dfs(1);
for(int i=1;i<=n;i++)
Insert(Xor[i]);//插入trie树
for(int i=1;i<=n;i++)
res=max(res,search(Xor[i]));
printf("%d\n",res);
return 0;
}
④【bzoj3012】[Usaco2012 Dec]First
分析:
若是想让字符串S为所有字符串中以自定义字典序最小,那么我们设还存在一个字符串T,两个字符串的公共前缀长度为LEN,若在自定义的字典序下,S<T那么必有S[LEN]<T[LEN] (字符串初始下标从0开始),那么我们想映射这样一种关系,可以采取建图的方式,设u=S[LEN],v=T[LEN],那么我们从u出发引一条有向边到v。判断自定义字典序是否成立,只需要判断建的图是否存在环,那么我们可以使用拓扑排序来验证,至于公共前缀,我们可以通过Trie树来找。
时间复杂度:O(26*字符总数)
SOLUTION:
#include<bits/stdc++.h>//trie树+拓扑排序 时间复杂度O(26*字符总数)
using namespace std;
const int N=31000;
const int M=3e5+10;
int n,trie[M][26],tot=1,res,In_du[26]/*26个字母每个字母的入度*/,To[26][26]/*To[i][j]=1,表示'a'+i这个字符在规定字典序中小于a+'j',即拓扑排序图中i->j有一条有向边*/;
bool End[M],T[M];
string s[N];
queue<int> q;
void Insert(string s){
int len=s.size(),p=1;
for(int i=0;i<len;i++){
int ch=s[i]-'a';
if(trie[p][ch]==0) trie[p][ch]=++tot;
p=trie[p][ch];
}
End[p]=1;
}
bool topsort(){//通过拓扑排序来验证是否有环,若有环则该字典序不成立
while(!q.empty()) q.pop();
for(int i=0;i<26;i++){
if(In_du[i]==0) q.push(i);
}
while(!q.empty()){
int f=q.front();
for(int i=0;i<26;i++){
if(To[f][i]==1){
In_du[i]--;
if(In_du[i]==0) q.push(i);
}
}
q.pop();
}
for(int i=0;i<26;i++)
if(In_du[i]>0) return 0;//存在有点入度大于0,存在环
return 1;
}
bool check(string s){//检验字符串s在一定的字典序下能否成为s[1~n]中字典序最小的字符串
memset(In_du,0,sizeof(In_du));
memset(To,0,sizeof(To));
int len=s.size(),p=1;
for(int i=0;i<len;i++){
int ch=s[i]-'a';
if(End[p]) return 0;//有当前字符串的前缀字符串,肯定不成立
for(int j=0;j<26;j++){
if(j!=ch && trie[p][j]!=0/*想了好久才明白:只有跟当前字符串s1有相同前缀的字符串s2,即s1[1~i-1]=s2[1~i-1],且s1[i]!=s2[i]时,要让s1的字典序小于s2,则需要加边*/ && To[ch][j]==0)
To[ch][j]=1,In_du[j]++;
}
p=trie[p][ch];
}
return topsort();
}
int main(){
freopen("first.in","r",stdin);
freopen("first.out","w",stdout);
scanf("%d",&n);
for(int i=1;i<=n;i++){
cin>>s[i];
Insert(s[i]);
}
for(int i=1;i<=n;i++){
if(check(s[i])){
T[i]=1;
res++;
}
}
printf("%d\n",res);
for(int i=1;i<=n;i++){
if(T[i]) cout<<s[i]<<endl;
}
return 0;
}