字典树
作者:互联网
定义
(本篇仅代表本人自己的观点,请路过大佬勿喷)
字典树,又称单词查找树,Trie树,是一种树形结构,是一种哈希树的变种,适用于实现字符串快速检索的多叉树,典型应用是用于统计,排序和保存大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较,查询效率比哈希树高
那么她是如何实现的呢?Tire的每个节点都拥有若干个字符指针,若在插入或检索字符串时扫描到一个字符c,就沿着当前节点的c字符指针,走向该指针指向的节点,以此类推......
这样讲感觉太笼统了,有点难理解,个人觉得可以理解成翻字典,找到一个英文字母以后,继续找下一个,这是同样的道理,接下来让我们看看字典树的基本操作过程
基本操作过程
其实就两个操作--插入(insert)和查询(find)也可以称之为检索,(差不多啦,这个没必要纠结,喜欢什么用什么)
插入(insert)
字典树也是一棵树,虽然简单,但人家也是有尊严的!想要用她,就得建立她,(咳......这个Ta用女字旁纯属个人喜好,更生动嘛!)那该如何建立呢?就是用这个插入操作,比如有很多的单词,那就把这些单词的每个字母都插进去,等你把单词插完了,她就建好了!举个例子,比如下面这棵树-->
这棵树就包括了(cap,cat,csp,co,code)这么几个单词,这样是不是感觉直观多了!
那着上面的数字代表什么?其实是编号,每个结点的编号,因为在插入过程中,如果当前要插入的字符在树上没有,那么就要将该字符作为一个新的节点,那肯定不能乱了顺序,在这个文明社会得知道先来后到吧(扯歪了),所以就需要一个全局变量tot来记录节点编号,那如果已经有当前要插入的字符,比如说第二个单词(cat)他的前缀“ca”在上一个单词(cap)中已经被插入了,那既然都有了,那还插什么?直接就跳到该字符所在的节点就行了!
那这样看来的话,称这颗树为前缀树似乎更好理解,因为两个单词如果有重合的前缀,那么他们就共用,直到前缀不一样或者是一个单词就是另一个单词的前缀,才会出现不同,那很多的单词也同样的道理,也可以通过相同的前缀放到一棵树上,有没有感觉豁然开朗?
但是,又有一个问题,如果没有相同的前缀呢,那咋整啊?那要搞一个虚拟节点呗!所以字典数会有一个固定的根节点--root,它可以是不会被用到的任意节点编号,0或1,看个人习惯吧,我习惯用0
知道思路和会实现代码是两码事,这里我直接贴上我习惯写法的代码
1 const int N=1000005; 2 int t[N][26],root=0,tot=2;//t数组用于存储字典树 t[4][2]表示,4号节点下面2这个儿子 3 void insert(string s)//传入一个单词 4 { 5 int p=root;//p是指当前在树上的位置,一开始固定在根上 6 for(int i=0;i<s.size();i++)//将单词扫描一边 7 { 8 int id=s[i]-'a';//将每个字母单独拿出来 9 if(t[p][id]==0) t[p][id]=tot++;//当前位置如果没有id这个儿子,就新建节点 10 p=t[p][id];//跳到下一个节点 11 } 12 }
好了,插入操作讲完了,已经可以进行简单的运用了,如果想先插入的巩固知识的话,可以做一下简单的题目实战一下!!
P1481 魔族密码 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
这个题目就是插入的一道很裸的题目了,可以自行AC,没有任何问题!
接下来还有一个重要的操作
查询(find)
真个操作可以用来查找一个单词是否在这棵树上,也能用来查前缀(哎呀,其实你想想查什么都行,但查你女朋友户口本应该不行),在建好树的基础上可以进行这个操作,下面是查有没有这个单词的代码
1 const int N=1e6+5; 2 int root=0,t[N][26],flag[N]; 3 int find(sting s) 4 { 5 int p=root; 6 for(int i=0;i<s.size();i++) 7 { 8 int id=s[i]-'a'; 9 if(t[p][id]==0) return 0;//一旦找不到,直接返回 10 p=t[p][id]; 11 } 12 if(flag[p]) return 1;//这里必须判断一下,flag[4]指的是在4这个节点有单词结尾,不判断的话可能会因为找到其他单词的前缀而出错 13 }
其实说实话,我感觉查询和插入跟双胞胎一样的,都是差不多的,没变多少,查询也可以根据题意调整
下面看一道例题:P2580 于是他错误的点名开始了 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
这道题是一道很裸很裸的例题,就是 有 手 就 行!!
我贴一下我的AC代码,仅供参考
1 #include<bits/stdc++.h> 2 using namespace std; 3 const int N=1000005; 4 int t[N][26],root=0,tot=2; 5 bool vis[N],f[N]; 6 void insert(string s) 7 { 8 int p=root; 9 for(int i=0;i<s.size();i++) 10 { 11 int id=s[i]-'a'; 12 if(t[p][id]==0) t[p][id]=tot++; 13 p=t[p][id]; 14 } 15 f[p]=1; 16 } 17 void query(string s) 18 { 19 int p=root; 20 for(int i=0;i<s.size();i++) 21 { 22 int id=s[i]-'a'; 23 if(!t[p][id]) 24 { 25 cout<<"WRONG"<<endl; 26 return ; 27 } 28 p=t[p][id]; 29 } 30 if(f[p]==0) 31 { 32 cout<<"WRONG"<<endl; 33 return ; 34 } 35 if(vis[p]==1) 36 { 37 cout<<"REPEAT"<<endl; 38 return ; 39 } 40 else 41 { 42 cout<<"OK"<<endl; 43 vis[p]=1; 44 } 45 } 46 int main() 47 { 48 int n,m; 49 string s; 50 cin>>n; 51 for(int i=1;i<=n;i++) cin>>s,insert(s); 52 cin>>m; 53 for(int i=1;i<=m;i++) cin>>s,query(s); 54 return 0; 55 }
一开始都说了,字典树不仅可以处理字符串,同样可以处理其它数据,比如下面的
字符串的其他应用
这次直接用一道例题方便大家理解
题意大家应该很清楚吧,就是在一棵树上找两个点,使他们的异或路径最大
分析:
因为题目中并没有给定根节点,所以我们很容易就能想到,指定节点1为根节点,使其变成有根树,建立这棵树,求第i个节点到根节点的异或路径si(递归求解),而节点i到节点j的异或路径就是si异或sj,这是两个节点分别到根节点的路径,因为路径是唯一的,在他们lca之上的部分是重叠的,异或两遍会归零,因此,对于每个si,找到一个sj使得路径最大即可求解,所以就能AC了(AC个屁,那和字典树有什么关系?)
是的,所以还有个问题,如果两两枚举的话,效率很低,所以今天的主角登场!!
先看下面的图片你就知道了
我们可以用字典树来维护这些数字,把这些数字看成一个31位的字符串,并建立字典树,每个叶节点的深度都是相同的,建完以后(如上图),对于每个到根异或路径值,可以用贪心的策略,从字典树的根开始,每一位使用贪心策略,如果字典树当前只有一个节点,直接往下走,如果有两个的话,选择和这一位不同的数构成1,当枚举完所有的位数,也就到了字典树的叶子节点了,正好找到了符合要求的sj
有没有感觉这个运用很妙?
下面是我的AC代码:
1 #include<bits/stdc++.h> 2 using namespace std; 3 const int N=1e5+10; 4 struct node 5 { 6 int v,w; 7 }; 8 vector<node>G[N]; 9 int n,tot,t[N*31][2],s[N],root=0; 10 void insert(int x) 11 { 12 int u=root; 13 for(int i=30;i>=0;i--) 14 { 15 int op=bool(x&(1<<i));//这一位是0还是1 16 if(!t[u][op]) t[u][op]=++tot; 17 u=t[u][op]; 18 } 19 } 20 void dfs(int u,int fa) 21 { 22 for(int i=0;i<G[u].size();i++) 23 { 24 int v=G[u][i].v,w=G[u][i].w; 25 if(v==fa) continue; 26 s[v]=s[u]^w;//和父节点的异或路径再异或一次 27 dfs(v,u); 28 } 29 } 30 int find(int x) 31 { 32 int ans=0,u=root; 33 for(int i=30;i>=0;i--) 34 { 35 int op=bool(x&(1<<i)); 36 if(t[u][!op]) //优先走使答案变成1的方向 37 { 38 ans+=(1<<i);//累加答案 39 u=t[u][!op]; 40 } 41 else u=t[u][op]; 42 } 43 return ans; 44 } 45 int main() 46 { 47 cin>>n; 48 for(int i=1;i<n;i++) 49 { 50 int x,y,w; 51 cin>>x>>y>>w; 52 G[x].push_back((node){y,w}); 53 G[y].push_back((node){x,w}); 54 } 55 dfs(1,-1);//根据读入建立树,计算每个到根节点的异或路径 56 for(int i=1;i<=n;i++) insert(s[i]); 57 int ans=0; 58 for(int i=1;i<=n;i++) ans=max(ans,find(s[i]));//对于每个si,都找的到对应的sj 59 cout<<ans;//输出答案,完美收官,撒花! 60 return 0; 61 }
这道题的链接:P4551 最长异或路径 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
习题:题目列表 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
剩下的关于字典树的内容,有兴趣的读者可以自行查阅学习!
谢谢大家阅读,码字不易,随手点赞,谢谢!!!
标签:int,单词,插入,root,节点,字典 来源: https://www.cnblogs.com/Small-Joker/p/zfc2.html