其他分享
首页 > 其他分享> > 字典树

字典树

作者:互联网

定义

(本篇仅代表本人自己的观点,请路过大佬勿喷)  

字典树,又称单词查找树,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