Trie树(字典树)
作用
看下面两个题:
- 给出n个单词和m个询问,每次询问一个单词,回答这个单词是否在单词表中出现过。
答: 简单!map,短小精悍。
- 给出n个单词和m个询问,每次询问一个前缀,回答询问是多少个单词的前缀。
答: map !TLE警告!
这就需要字典树
概念
单词查找树,Trie树,是一种树形结构,是一种
哈希树的变种。
优点:
- 利用字符串的公共前缀来节约存储空间,
- 最大限度地减少无谓的字符串比较,查询效率比哈希表高。
先放一张字典树的图:
可以发现,这棵字典树用边来代表字母,而从根结点到树上某一结点的路径就代表了一个字符串。
比如说字符串 caa,就是1->4->8->12。
知道思想,建树就很简单了。
代码
我们定义几个变量
-
son[N][26]:存放子节点对应的idx。
其中第一维是指:节点对应的idx
第二维是指:子节点('a' - '0')的下标。(或者说是指向下一个节点的边)
比如: son[1][0]=2表示1结点的一个值为a的子结点为结点2 -
cnt[N]:存放该idx对应的个数
-
idx: 记录每一个节点的位置。
建树
思路:
从左到右扫这个单词,如果字母在相应根节点下没有出现过,就插入这个字母;否则沿着字典树往下走,看单词的下一个字母。
代码:
void insert(char *str) { int p = 0; //类似指针,指向当前节点 for(int i = 0; str[i]; i++) { int u = str[i] - 'a'; //将字母转化为数字 if(!son[p][u]) son[p][u] = ++idx; //该节点不存在,创建节点,其值为下一个节点位置 p = son[p][u]; //使“p指针”指向下一个节点位置 } cnt[p]++; //结束时的标记,也是记录以此节点结束的字符串个数 }
查找
思路
从左往右以此扫描每个字母,顺着字典树往下找,能找到这个字母,往下走,否则结束查找,即没有这个单词。扫描完单词,则表示有这个单词。
代码
int query(char *str) { int p = 0; for(int i = 0; str[i]; i++) { int u = str[i] - 'a'; if(!son[p][u]) return 0; //该节点不存在,即该字符串不存在 p = son[p][u]; } return cnt[p]; //返回字符串出现的次数 }
模板代码
int son[N][26],cnt[N],idx; void insert(string s){ int p=0; for(int i=0;i<s.length();i++){ int u=s[i]-'a'; if(!son[p][u]) son[p][u]=++idx; p=son[p][u]; } cnt[p]++; } int query(string s){ int p=0; for(int i=0;i<s.length();i++){ int u=s[i]-'a'; if(!son[p][u]) return 0; p=son[p][u]; } return cnt[p]; }
应用
检索字符串
字典树最基础的应用——查找一个字符串/前缀是否在“字典”中出现过。
字典树 模板题
#include <cstdio> #include <cstring> #include <algorithm> using namespace std; struct ooo { int next[26]; bool mark; //标记 }dd[505555]; int top,n,m; char a[20],b[20]; int creat() //分配新的节点 { memset(dd[top].next,-1,sizeof(dd[top].next)); //表示指向空 dd[top].mark=false;//表示无对应字符串 return top++; } int xiab(char c) { return c-'a'; //对应下标 } void insert(int root,char *s) //将S插入到字典树中 { int i,len=strlen(s); for(i=0;i<len;i++) { if(dd[root].next[xiab(s[i])]==-1) dd[root].next[xiab(s[i])]=creat(); root=dd[root].next[xiab(s[i])]; } dd[root].mark=true; } bool search(int root,char *s) //询问是否出现在字典树中 { for(int i=0;s[i]!='\0';i++) { if(dd[root].next[xiab(s[i])]==-1) return false; root=dd[root].next[xiab(s[i])]; } return dd[root].mark; } int main() { int i,j,root; while(scanf("%d %d",&n,&m)&&(n||m)) { top=0; root=creat(); for(i=0;i<n;i++) { scanf("%s",a); insert(root,a); } for(i=0;i<m;i++) { scanf("%s",b); printf("%s\n",search(root,b)?"Yes":"No"); } } return 0; }
思路:
题目要求后缀,反转成前缀即可。
但是...数据量太大,字典树会超时,题目就变成了思维题。
#include<stdio.h> #include<iostream> #include<algorithm> #include<string.h> using namespace std; const int N=1e5+10; int col[N]; int a[N][15]; int flag[N]; int k; string s1,s2; void Insert() { int p=0,i; for(i=0;i<s1.size();i++) { if(!a[p][s1[i]-'0']) a[p][s1[i]-'0']=++k; col[p]++;//记录几个单词经过了他。 p=a[p][s1[i]-'0']; } flag[p]=1;//表示这是一个单词的末尾。 } int search1() { int p=0,i; for(i=0;i<s2.size();i++) { if(!a[p][s2[i]-'0']) return 0; p=a[p][s2[i]-'0']; } return col[p]; } int main() { int n,m,i; ios::sync_with_stdio(false); while(cin>>n) { for(i=1;i<=n;i++) { cin>>s1; reverse(s1.begin(),s1.end()); Insert(); } cin>>m; for(i=1;i<=m;i++) { cin>>s2; reverse(s2.begin(),s2.end()); int num=search1();//找到单词段然后返回值。 printf("%d\n",num); } memset(flag,0,sizeof(flag)); memset(a,0,sizeof(a)); memset(col,0,sizeof(col)); k=0;//初始化。 } return 0; }
AC 自动机
trie 是 AC 自动机 的一部分。
维护异或极值
将数的二进制表示看做一个字符串,就可以建出字符集为{0,1} 的 trie 树,称为。
如果将所有数以二进制形式插入到一棵 trie 中,就可以快速求出和 数字T 的异或和最大的数:
从 trie 的根开始,如果能向和 T 的当前位不同的子树走,就向那边走,否则走相同位的方向。
143. 最大异或对
题意:
在给定的N个整数中选出两个进行xor(异或)运算,得到的结果最大是多少?
思路:
将每个数以二进制方式存入字典树,找的时候从最高位去找有无该位的异.
代码:
#include <bits/stdc++.h> #define ins 0x3f3f3f3f using namespace std; const int N = 100010, M = 3100010; #define pii pair<int, int> int n; int son[M][2],idx; int a[N]; void insert(int x){ int p=0; for(int i=30;i>=0;i--){ int k=(x>>i)&1; if(!son[p][k]) son[p][k]=++idx; p=son[p][k]; } } int query(int x){ int p=0; int res = 0; for(int i=30;i>=0;i--){ int k=(x>>i)&1; if(son[p][!k]) { res += 1 << i; p=son[p][!k]; } else p=son[p][k]; } return res; } void solve() { int n; cin >> n; for(int i=0;i<n;i++){ cin>>a[i]; insert(a[i]); } int ans=-1; for(int i=0;i<n;i++) ans=max(query(a[i]),ans); cout<<ans<<endl; } signed main() { ios::sync_with_stdio(false); cin.tie(0); cout.tie(0); solve(); return 0; }
01-trie 维护异或和
01-trie 是指字符集为 {0,1}的 trie。
01-trie 可以用来维护一些数字的异或和,支持
- 修改(删除 + 重新插入)
- 全局加一(即:让其所维护所有数值递增 1,本质上是一种特殊的修改操作)。
如果要维护异或和,需要按值从低位到高位建立 trie。
插入 & 删除
如果要维护异或和,我们 只需要 知道某一位上 0 和 1 个数的 奇偶性 即可,而不需要知道 trie 到底维护了哪些数字。
也就是对于数字 1 来说,当且仅当这一位上数字 1 的个数为奇数时,这一位上的数字才是 1。+
其余看oi-wiki吧
全局加一
所谓全局加一就是指,让这棵 trie 中所有的数值 +1。
我们思考一下二进制意义下 +1 是如何操作的:
我们只需要从低位到高位开始找第一个出现的 0,把它变成 1,然后这个位置后面的 1 都变成 0 即可。
对应 trie 的操作,其实就是交换其左右儿子,顺着 交换后 的 0 边往下递归操作即可。
01-trie 合并
01 trie 的合并和分裂和线段树没啥区别。
可持久化字典树
和其他可持久化数据结构没啥区别。
https://oi-wiki.org/ds/persistent-trie/
本文作者:kingwzun
本文链接:https://www.cnblogs.com/kingwz/p/15843639.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步