ICode9

精准搜索请尝试: 精确搜索
首页 > 其他分享> 文章详细

字典树

2022-08-22 15:31:30  阅读:134  来源: 互联网

标签:int 单词 插入 root 节点 字典


定义

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

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

本站声明: 1. iCode9 技术分享网(下文简称本站)提供的所有内容,仅供技术学习、探讨和分享;
2. 关于本站的所有留言、评论、转载及引用,纯属内容发起人的个人观点,与本站观点和立场无关;
3. 关于本站的所有言论和文字,纯属内容发起人的个人观点,与本站观点和立场无关;
4. 本站文章均是网友提供,不完全保证技术分享内容的完整性、准确性、时效性、风险性和版权归属;如您发现该文章侵犯了您的权益,可联系我们第一时间进行删除;
5. 本站为非盈利性的个人网站,所有内容不会用来进行牟利,也不会利用任何形式的广告来间接获益,纯粹是为了广大技术爱好者提供技术内容和技术思想的分享性交流网站。

专注分享技术,共同学习,共同进步。侵权联系[81616952@qq.com]

Copyright (C)ICode9.com, All Rights Reserved.

ICode9版权所有