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 树,称为\(01-trie\)。
如果将所有数以二进制形式插入到一棵 trie 中,就可以快速求出和 数字T 的异或和最大的数:
从 trie 的根开始,如果能向和 T 的当前位不同的子树走,就向那边走,否则走相同位的方向。
143. 最大异或对
题意:
在给定的N个整数\(A_1,A_2 …… A_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/