浅谈字典树
本篇文章同步发表在博客园。
字典树
什么是字典树?
字典树,顾名思义它是一棵类似于字典的树,用树的形态存储字符串集合。具体地,它有一个自定义的无意义的根节点(通常编号为
字典树的插入、删除与查询
插入
插入,是字典树中特别常用的操作,其表示在一棵选定的字典树中插入某个字符串
假定要插入的字符串
考虑使用
具体插入的时候,用当前所处节点编号
示例代码(根节点定为
void Add(string s){
int now=0;
for(char c:s){
int x=c-'a';
if(!tree[now][x])tree[now][x]=(++Cnt);
now=tree[now][x];
}
return;
}
删除
删除其实很麻烦的,因为你只是一个字符串不可用了,但那个节点可能承载了不只一个字符串,这个时候你就要判断啦,如果除了当前这个字符串以外还有别的字符串要经过这个点,那你就不能删掉这个点。
为了实现以上这一步,咱也得先把 Add 函数改一下——毕竟现在得记录每个点承载了多少个字符串呀!
具体的修改是很简单的,加个
所以把 Add 函数改成这样即可:
void Add(string s){
int now=0;
for(char c:s){
int x=c-'a';
if(!tree[now][x])tree[now][x]=(++Cnt);
now=tree[now][x],f[now]++;
}
return;
}
但是呢,你也不可能真的去删掉一个节点,那样未免也太麻烦啦。
怎么办呢?其实并不需要打标记或者什么的,删除节点的时候只需要让
所以 Del 函数也就简单明了了:
void Del(string s){
int now=0;
for(char c:s){
int x=c-'a';
now=tree[now][x],f[now]--;
}
return;
}
查询
查询分很多种,这里只讲一下最基本的,即判断字典树中目前是否存在某个指定字符串。
跟 Add 函数很相似,但不同的是,如果没有连边就直接返回
代码大概就是这样的吧:
bool Ask(string s){
int now=0;
for(char c:s){
int x=c-'a';
if(!tree[now][x]||!f[tree[now][x]])
//这里考虑了存在删除操作的情况,
//如果没有删除操作就不需要判断 f 数组了
return false;
now=tree[now][x];
}
return true;
}
例题一:CF1792D
很经典的题目。
容易发现其是一个显然的字典树,在 Ask 的时候动点手脚,不是判断能不能走到底,而是算出最多能够走多少——甚至都不需要 Del 函数!
然后你就非常光荣地,错在了样例上。
为什么?因为根据题目要求,我们不能直接拿着这一堆的数组来建字典树,而是得把它反过来——假设原来下标是
归根到底,还是因为题目中这种运算的设定。并且题目也是问的“对于所有
然后你就 A 了。代码实现的难度是很小的。
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define pii pair<int,int>
#define fr first
#define se second
#define pb push_back
using namespace std;
const int N = 5e5+5;
int T,n,m,a[N][15],s[N][15],tree[N][15],Cnt;
int read(){
int su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
void Init(){
for(int i=0;i<=Cnt;i++)
for(int j=0;j<=10;j++)tree[i][j]=0,s[i][j]=0;
Cnt=0;return;
}
void Add(int id){
int now=0;
for(int i=1;i<=m;i++)cin>>a[id][i];
for(int i=1;i<=m;i++)s[id][a[id][i]]=i;
for(int i=1;i<=m;i++){
int x=s[id][i];
if(!tree[now][x])tree[now][x]=(++Cnt);
now=tree[now][x];
}
return;
}
int Ask(int id){
int now=0,ans=0;
for(int i=1;i<=m;i++){
int x=a[id][i];
if(!tree[now][x])return ans;
now=tree[now][x],ans++;
}
return ans;
}
int main(){
T=read();
while(T--){
n=read(),m=read();Init();
for(int i=1;i<=n;i++)Add(i);
for(int i=1;i<=n;i++)cout<<Ask(i)<<" ";cout<<"\n";
}
return 0;
}
例题二:AT_agc047_b
为了简化描述,之后将字符串的变化情况视作
题目有句话,说,当
我们来考虑一下这句话的意思。首先
并且呢,如果上一步删去了
那么上字典树就好了。
实现很简单,在 Ask 函数里多算点东西就好了。不过由于计算时需要 Del 函数但还是要计算
具体……看代码吧。
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define pii pair<int,int>
#define fr first
#define se second
#define pb push_back
using namespace std;
const int N = 1e6+5;
int n,Cnt,tree[N][30];
LL f[N],Ans,p[30];string s[N];
void Add(string str){
int now=0;
for(int k=str.size()-1;k>=0;k--){
int x=str[k]-'a';
if(!tree[now][x])tree[now][x]=(++Cnt);
now=tree[now][x];
}
f[now]++;return;
}
LL Ask(string str){
memset(p,0,sizeof(p));
for(int k=0;k<str.size();k++)p[str[k]-'a']++;
int now=0;LL res=0;
for(int k=str.size()-1;k>0;k--){
int x=str[k]-'a';
for(int i=0;i<26;i++)
if(p[i])res+=f[tree[now][i]];
now=tree[now][x],p[x]--;
}
return res;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);cin>>n;
for(int i=1;i<=n;i++){cin>>s[i];Add(s[i]);}
for(int i=1;i<=n;i++)Ans+=Ask(s[i]);
cout<<Ans<<"\n";
return 0;
}
0/1 Trie
什么是 0/1 Trie?
0/1 Trie,即 0/1 字典树,是普通字典树的子问题之一。与普通字典树不同的,其只存储
其一般用来存储二进制下的一些数字,常用于解决有关位运算(尤其是异或)的题目。
0/1 Trie 的插入、删除与查询
与普通字典树类似,故不多赘述。
但是注意两个区别:
- 普通字典树在插入、删除与查询的时候都是根据当前传参的长度灵活调整的,因为只需要固定起点即可,但由于 0/1 Trie 多解决位运算问题,为实现贪心思想必须倒着来,从
8 到4 到2 再到1 ,因此枚举的时候一般固定从某值倒着枚到0 。通常上界为30/32/60/63/64 。 -
具体的实现可以参见下面两个例题的代码。
例题三:CF665E
这个思路比较一眼,吧。
定义
那么区间
对,你说得对,就是前缀和的一个形式,只不过是前缀异或和罢了。
然后建 Trie 跑就完了。具体细节不多说了。
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define pii pair<int,int>
#define fr first
#define se second
#define pb push_back
using namespace std;
const int N = 1e6+5 , M = 3e7+5;
int n,tree[M][2],Cnt=1;
LL k,a[N],sum,f[M],Ans;
LL read(){
LL su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
void Add(LL num){
LL now=1;
for(LL i=30;i>=0;i--){
LL x=(num>>i)&1;
if(!tree[now][x])tree[now][x]=(++Cnt);
now=tree[now][x],f[now]++;
}
return;
}
LL Ask(LL A,LL B){
LL now=1,res=0;
for(LL i=30;i>=0;i--){
LL x=(A>>i)&1,y=(B>>i)&1;
if(y)now=tree[now][1-x];
else res+=f[tree[now][1-x]],now=tree[now][x];
}
return res+f[now];
}
int main(){
n=read(),k=read();
for(LL i=1;i<=n;i++){
a[i]=read();
Add(sum),sum^=a[i];
Ans+=Ask(sum,k);
}
cout<<Ans<<"\n";
return 0;
}
例题四:CF1980G
这个题目比较复杂,我讲详细一点。
首先定义这棵树的根为节点
定义
易知
显然每次操作二的答案