【学习笔记】带你从0开始学习 01Trie

虚幻大学 xuhss 176℃ 0评论

? 优质资源分享 ?

学习路线指引(点击解锁) 知识定位 人群定位
? Python实战微信订餐小程序 ? 进阶级 本课程是python flask+微信小程序的完美结合,从项目搭建到腾讯云部署上线,打造一个全栈订餐系统。
?Python量化交易实战? 入门级 手把手带你打造一个易扩展、更安全、效率更高的量化交易系统

1|*0***01Trie**

1|*1***Section 1:普通 Trie**

1|*0***Section 1.1 什么是 Trie**

Trie 树,即字典树,是一种树形结构。典型应用是用于统计和排序大量的字符串前缀来减少查询时间,最大限度地减少无谓的字符串比较。

9db0e6c03a2d9f96f82c097662753532 - 【学习笔记】带你从0开始学习 01Trie


1|*0***Section 1.2 如何实现**

具体地说,对于每个结点,我们要保存几个信息:

  • ch[26] ,保存此字符的下一个字符(a∼za∼za\sim z)的存储地址(没有为 000)。
  • cnt ,保存此节点被经过了多少次。

对于整个 Trie 树,我们还要额外保存

  • Tcnt,为节点数。
  • Endp[],表示的是这个字符串是否以这个下标结尾(如果只是看是否是前缀,则不需要此数组)。

1|*0***几个操作**

  1. insert:往 Trie 树里插入一个字符串。

具体实现:把字符串里的字符扫描一遍,设当前字符为 sss,如果 ch[s-'a'] 不等于 000,跳转到 ch[s-'a'] 的存储下标。否则把 ch[s-'a'] 设为树的节点数加一,然后跳转。跳转时把当前节点的 cntcntcnt 加一。

跳到最后把当前节点 EndpEndpEndp 设为一。

  1. find:查询 Trie 里是否有这个字符串。

具体实现:根据每个字符一个一个跳。

如果跳的时候 cnt=0cnt=0cnt=0,说明没有。
如果跳到最后,有,但是 EndpnowNode=0EndpnowNode=0Endp_{nowNode}=0 即并不是以这个字符结尾的,说明没有。
否则有这个字符串,返回 true


1|*0***Section 1.3 代码实现**

(只给出基础的插入和查询出现次数)

点击查看代码

#include 
using namespace std;

const int N=3000005;

struct Trie{
 int T[N][63],Tsiz,Endp[N];
 void init(){
 for(int i=0;i<=Tsiz;i++) for(int j=0;j<=62;j++) T[i][j]=0;
 for(int i=1;i<=Tsiz;i++) Endp[i]=0;
 Tsiz=0;
 }
 int gethash(char ch){
 if(islower(ch)) return ch-'a';
 if(isupper(ch)) return ch-'A'+26;
 if(isdigit(ch)) return ch-'0'+26+26;
 }
 void insert(string s){
 int rt=0,len=s.length();
 for(int i=0;i int x=gethash(s[i]);
 if(!T[rt][x]) T[rt][x]=++Tsiz;
 rt=T[rt][x];
 }
 Endp[rt]++;
 }
 int find(string s){
 int rt=0,len=s.length();
 for(int i=0;i int x=gethash(s[i]);
 if(!T[rt][x]) return 0;
 rt=T[rt][x];
 }
 return Endp[rt];
 }
}trie;

int T,n,q;

int main(){
 trie.init();
 cin>>n>>q;
 string s;
 for(int i=1;i<=n;i++){
 cin>>s;
 trie.insert(s);
 }
 for(int i=1;i<=q;i++){
 cin>>s;
 cout<'\n';
 }
}
折叠 

1|*0***Section 1.4 模板**

【洛谷模板题 Link】

(使用以上代码无法通过此题,请写 cntcntcnt 维护前缀)


1|*2***Section 2:01Trie**

1|*0***Section 2.1 什么是 01Trie?**

和普通 Trie 相似,但是每个节点只有两个值:0/10/10/1。

从根节点至下的一条路径保存着一个正整数从高到低的二进制位。

如下图:

d3afe18f573bb6fd8f7c6dd8bc11b9bc - 【学习笔记】带你从0开始学习 01Trie

中序遍历结果为(忽略根节点): 00 01 10 1100 01 10 1100\ 01\ 10\ 11

我们会发现几点有趣的性质:

  • 01Trie 是一棵二叉树,每个节点的左儿子为 000,右儿子为 111。
  • 从根节点往下,所有左儿子开始的路径值都小于右儿子开始的路径值。

运用这两点性质,我们就可以用 01Trie 造一棵平衡树。


1|*0***Section 2.2 平衡树**

对于每个节点,我们维护两个信息:

  • siz,维护以当前节点为根节点的子树大小。
  • cnt,维护数字到当前节点为二进制的最后一位的数字个数。

此外,和普通 Trie 一样,我们还要维护树的大小 ppp。


1|*0***几个操作**

  1. insert:平衡树的插入操作。首先,给每个经过的结点的 sizsizsiz 加一,表示子树节点的个数多了一个。如果当前数字的当前二进制位为 000,就把他放在左儿子,否则放在右儿子。插入到最后一位时给当前节点的 cntcntcnt 加 111。
  2. delete:平衡树的删除操作,与 insert 几乎一样,只是把最后一位 cnt−1cnt−1cnt-1 就行了。
  3. get_rank:查询当前数的排名。从根节点开始往下,如果当前数的当前二进制位为 111,就把排名加上它的左子树的值(性质二)。
  4. get_kth:查询排名为 kkk 的数。从根节点往下,设它的左子树 sizsizsiz 为 tmptmptmp,则如果 k≤tmpk≤tmpk \le tmp,则说明排名为 kkk 的节点在它的左子树上。否则向右子树查找,并把答案的当前二进制位设为 111。
  5. pre:求当前数的前驱。返回 get_kth(get_rank(x)) 即可。
  6. nxt:求当前数的后继。返回 get_kth(get_rank(x+1)+1) 即可。

1|*0***Section 2.3 代码实现**

点击查看代码

#include 
using namespace std;

const int MAXLOG=24;
const int N=1e7;

class \_01trie{
private:
 struct node{
 int ch[2];
 int siz,cnt;
 }T[1< int p;
public:
 void update(int x,int offset){
 int now=0;
 for(int i=MAXLOG-1;i>=0;i--){
 bool now\_bit=(x&(1< if(T[now].ch[now\_bit]==0) 
 T[now].ch[now\_bit]=++p;
 now=T[now].ch[now\_bit];
 T[now].siz+=offset;
 }
 T[now].cnt+=offset;
 }
 int get\_rank(int x){
 int now=0,ans=0;
 for(int i=MAXLOG-1;i>=0;i--){
 bool now\_bit=(x&(1< if(now\_bit==1)
 ans+=T[T[now].ch[0]].siz;
 now=T[now].ch[now\_bit];
 if(now==0)
 break;
 }
 return ans;
 }
 int get\_kth(int k){
 int now=0,ans=0;
 for(int i=MAXLOG-1;i>=0;i--){
 int tmp=T[T[now].ch[0]].siz;
 if(k<=tmp)
 now=T[now].ch[0];
 else{
 k-=tmp;
 now=T[now].ch[1];
 ans|=(1< }
 if(now==0)
 break;
 }
 return ans;
 }
 int pre(int x){
 return get\_kth(get\_rank(x));
 }
 int nxt(int x){
 return get\_kth(get\_rank(x+1)+1);
 }
}Trie;

int n;
int opt,x;

int main(){
 scanf("%d",&n);
 for(int i=1;i<=n;i++){
 scanf("%d%d",&opt,&x);
 if(opt==1) Trie.update(x+N,1);
 if(opt==2) Trie.update(x+N,-1);
 if(opt==3) printf("%d\n",Trie.get\_rank(x+N)+1);
 if(opt==4) printf("%d\n",Trie.get\_kth(x)-N);
 if(opt==5) printf("%d\n",Trie.pre(x+N)-N);
 if(opt==6) printf("%d\n",Trie.nxt(x+N)-N);
 }
}

折叠 

1|*0***Section 2.4 模板**

【洛谷模板题 Link】

1|*3***Section 3:01Trie 例题**

洛谷 P4551 最长异或路径

Description
给定一棵树和树上的边权,求两点之间路径异或值的最大值。

Solution

先给定一个前置知识:x⊕x=0x⊕x=0x \oplus x=0。

所以树上两点路径的异或值等于根节点分别向两点的路径异或值的异或(root→LCA(x,y)root→LCA⁡(x,y)root \to \operatorname{LCA} (x,y) 那一段的异或值被抵消了)。

所以我们可以一遍 dfs 处理出根节点到所有节点的边权异或值,随后把它们扔进 01Trie 中。为了让异或值最大,我们可以枚举节点,在 Trie 上贪心:从高到低位,尽量选和当前数二进制位不一样的,随后再与原节点异或值异或一下,取 maxmax\max 即可。

Code

点击查看代码

#include 
using namespace std;

const int MAXLOG=31;
const int N=3e5+5;

class \_01trie{
private:
 struct node{
 int ch[2];
 int siz,cnt;
 }T[N<<5];
 int p;
public:
 void update(int x){
 int now=0;
 for(int i=MAXLOG-1;i>=0;i--){
 bool b=(x&(1<<i));
 if(T[now].ch[b]==0) 
 T[now].ch[b]=++p;
 now=T[now].ch[b];
 }
 }
 int query(int x){
 int now=0,ans=0;
 for(int i=MAXLOG-1;i>=0;i--){
 bool b=(x&(1<<i));
 if(T[now].ch[b^1]){
 ans=ans<<1|(b^1);
 now=T[now].ch[b^1];
 }
 else{
 ans=ans<<1|b;
 now=T[now].ch[b];
 }
 }
 return ans;
 }
}Trie;

int n;

struct Graph{
 int to,w,next;
}G[N<<1];
int head[N],cnt;
void addEdge(int u,int v,int w){G[++cnt]=(Graph){v,w,head[u]}; head[u]=cnt;}

int val[N];

void dfs(int x,int fa){
 for(int i=head[x];i;i=G[i].next){
 int t=G[i].to;
 if(t==fa) continue;
 val[t]=val[x]^G[i].w;
 dfs(t,x);
 }
}

int main(){
 cin>>n;
 for(int i=1;i<n;i++){
 int u,v,w;
 cin>>u>>v>>w;
 addEdge(u,v,w);
 addEdge(v,u,w);
 }
 dfs(1,0);
 for(int i=1;i<=n;i++) Trie.update(val[i]);
 int maxx=0;
 for(int i=1;i<=n;i++) maxx=max(maxx,Trie.query(val[i])^val[i]);
 cout<}
折叠 

__EOF__

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qicki5uq-1658423572350)(https://blog.csdn.net/LByte/p/16502346.html)]TheSky233 本文链接:https://blog.csdn.net/LByte/p/16502346.html关于博主:评论和私信会在第一时间回复。或者直接私信我。版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!

转载请注明:xuhss » 【学习笔记】带你从0开始学习 01Trie

喜欢 (0)

您必须 登录 才能发表评论!