字典树Trie +字符串'01字典树 + 最大异或对 +最大异或和 + 最大异或路径
字典树基本内容的介绍讲解
今天,咱们来聊下字典树;
首先,几个问题
Trie是什么?
“Trie这个名字取自“retrieval”,检索,因为Trie可以只用一个前缀便可以在一部字典中找到想要的单词。”
查询就需要一定的媒介作为支撑,树就为这种查询提供支撑。
故字典树肯定是一个跟字典一样在一棵树上能够查询特定的数据(可以是字母类型的,二进制类型的等等);*
Trie做什么 ?
实现字符串快速检索的多叉树结构。
常见的字符串转化:小写字母或者大写字母组成的字符串,数字组成的字符串,01编码组成的字符串
因为某节点的儿子存在共同的前缀
Trie为什么快?
核心思想 : 空间换时间,利用字符串的公共前缀来降低查询时间的开销以达到提高效率的目的
Trie能干什么?
- 插入(将一个字符串插入到字典树上)
- 检索(检索一个字符串 S 在Trie 上是否存在)
应用场景 :
统计和排序大量的字符串
【注意有比较多的公共前缀这些字符串】,`
求最大异或对
文本词频统计
前缀匹配
字符串检索
怎么建一个Trie呢?
我们选取字符串作为例子,来说一下建树的过程
假如我想插入abcde,abdef,abcf,ae,bc
我们从一个树的根开始向下建树
首先插入abcde到这个树上
第一步:检索a是否存在 ,有则把这个位置给他,不用额外创建,否则新建一个位置给他
显然,树为空的,我们给他新开辟一个idx;将指针idx指向它;
第二步:检索b是否存在,重复上述步骤,无则新辟,有则直接利用
....
然后插入abcf到这个树上
检索a是否存在,有,直接利用,继续往下走,b有,继续往下走,c有,继续往下走,f没有,不好意思,那就新开辟一个
一顿操作后,大概就是这个样子
建树代码实现
son[N][26]数组 (本例子是字符串,N是字符串长度,取数据范围的最大值,每个节点后都可能有26个子字母与其相连),来记录这棵字典树
idx 指针 ,每个节点都对应一个idx值,所以每次新开辟一个节点 idx就要++,如果存在的话,直接将该节点idx赋给他;
cnt[] 数组的值是该idx对应的个数。
用p来做操作当作 位置变量,开始从root开始,p=0,然后for(变量字符串)
代码实现就是这样
if(!son[p][u]) //没有的话++idx,并把此idx指向son[p][u] ,然后把son赋给p继续这样的遍历
son[p][u]=++idx;
p=son[p][u];
插入模板
const int N = 2e5+5;
int idx,cnt[N];
int son[N][26];
char str[N];
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];
}
cnt[p]++;
}
如何去查询呢?
查询操作和建树操作有点一样,就是有的话一直往下,没有对应的子节点,说明树中没有此串,return 0,代码和建树基本相同
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];
}
字典树的实例!
前缀统计
给定 N 个字符串 S1,S2…SN,接下来进行 M 次询问,每次询问给定一个字符串 T,求 S1∼SN 中有多少个字符串是 T 的前缀。
输入字符串的总长度不超过 106,仅包含小写字母。
输入格式
第一行输入两个整数 N,M。
接下来 N 行每行输入一个字符串 Si。
接下来 M 行每行一个字符串 T 用以询问。
输出格式
对于每个询问,输出一个整数表示答案。
每个答案占一行。
输入样例:
3 2
ab
bc
abc
abc
efg
输出样例:
2
0
代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e6+5;
char ch[N];
int son[N][26];
int cnt[N];
int 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];
}
cnt[p] ++;
}
int query(char str[])
{
int p = 0;
int ans = 0;
for(int i = 0; str[i]; i ++)
{
int u = str[i] - 'a';
if(!son[p][u])
return ans;
p = son[p][u];
ans += cnt[p];
}
return ans;
}
int main()
{
int n, m;
cin >> n >> m;
for(int i = 1; i <= n; i ++)
{
cin >> ch;
insert(ch);
}
while (m -- ){
cin >> ch;
cout<<query(ch)<<endl;
}
}
最大异或对 题目链接
input
10 10
1 2 3 4 5 6 7 8 9 10
1 2
1 3
1 5
2 6
2 8
2 10
3 7
8 10
6 9
1 7
output
3
3
7
7
15
15
7
3
15
7
题目思路
题的意思是给一个数组,每次询问一个区间[l,r],让你选择两个数,i,j,使得a[i]^a[j]最大
题数据范围是 5e3暴力for的话必要tle,我们可以用字典树来做一下
首先考虑下数据类型
让我们插入的数是一个十进制的数,范围在2^10
,如果插十进制,显然不能利用前缀的特点去插,于是我们可以考虑插入他的二进制
确定son[N][u] N,u的范围
**我们给son确定范围要从题目出发,范围在2^10
,说明每个数最大长度为10,N个数 就是 N*10,u的位置我们放的是 1或者0 所以 u是 2 故 son[10*
N][2]
然后看下如何建一棵二进制字典树
假如需要插入一个十进制数x,我们把他的二进制都取出来,去执行我们的插入操作,用 x>>i&1来判断这一位是否为1,其他操作和原来一样
插入代码
void insert(int x)
{
int p=0;
for(int i=10;i>=0;i--)
{ int u=x>>i&1;
if(!son[p][u])
son[p][u]=++idx;
p=son[p][u];
}
}
树建好了,怎么去查询最大异或对呢?
想一下,异或的性质
异或肯定是要优先找某一位的0,1异或
再考虑下如何query?
建完这个区间的树后,我们还是从根节点root,也就是0出发,用res来记录最大异或的答案,还是从低位开始,取出该位置是0还是1,如果是0的话,判断下son[p][!u]是否存在,存在的话最好了,记录这个答案res2+1;(为什么是res*
2,想一下十进制是如何加的 res*
10,同理这里是res2) 没有的话,没办法只好res*
2+0了
返回res就是此区间的最大异或对
int query(int x){
int p=0;
int res=0;
for(int i=10;i>=0;i--)
{
int u=x>>i&1;
if(son[p][!u])
{ p=son[p][!u];
res=res*2+1;
}
else
{
p=son[p][u];
res=res*2+0;
}
}
return res;
}
完整代码
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N=5e3+5;
int son[N*10][2],idx,a[N],cnt[N];
void insert(int x)
{
int p=0;
for(int i=10;i>=0;i--)
{ int u=x>>i&1;
if(!son[p][u])
son[p][u]=++idx;
p=son[p][u];
}
}
int query(int x){
int p=0;
int res=0;
for(int i=10;i>=0;i--)
{
int u=x>>i&1;
if(son[p][!u])
{ p=son[p][!u];
res=res*2+1;
}
else
{
p=son[p][u];
res=res*2+0;
}
}
return res;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
{ cin>>a[i];
}
while(m--)
{
int l,r;
cin>>l>>r;
for(int i=l;i<=r;i++)
{insert(a[i]);
}
int max1=0;
for(int i=l;i<=r;i++)
{
max1=max(max1,query(a[i]));
}
cout<<max1<<'\n';
for(int i = 0 ; i <= idx ; i ++)
{
son[i][0] = son[i][1] = 0;
}
idx = 0;
}
}
拔高,区间最大异或和
给定一个非负整数数列 a,初始长度为 N。
请在所有长度不超过 M 的连续子数组中,找出子数组异或和的最大值。
子数组的异或和即为子数组中所有元素按位异或得到的结果。
注意:子数组可以为空。
输入格式
第一行包含两个整数 N,M。
第二行包含 N 个整数,其中第 i 个为 ai。
输出格式
输出可以得到的子数组异或和的最大值。
数据范围
对于 20% 的数据,1≤M≤N≤100
对于 50% 的数据,1≤M≤N≤1000
对于 100% 的数据,1≤M≤N≤105,0≤ai≤231−1
输入样例:
3 2
1 2 4
输出样例:
6
来源:美团2021,笔试题
这个题加了一个限制条件,给定一个最大区间m,要求你去找到一个长度<=m的连续区间,使他们的异或和最大
首先,我们需要一个预处理过的前缀异或和数组S【N】;
令S[n]=a[1] ^ a[2] ^ a[3] ^ ... ^a[p-1] ^ a[p] ^.... ^ a[n]
那么S[n] ^ S[p-1] =a[p] ^.... ^ a[n] (前面的消掉了)
我们插入Trie树的数就可以是S[i],来代表区间异或和
其次,考虑下限制长度m,如果按照以前的思路,当我们插入了m组数据后,想要添加第m+1个,我们是不是需要再删除第1个,才能使区间长度为m,难道需要重新再建一棵树吗?想想都觉得太浪费了,我们可以动态维护一个Trie树,之前我们cnt[]记录的是某个数出现的次数,现在我们改变它的属性,让他代表子前缀p出现的次数,没出现一次该前缀,cnt[p]就++, 同样当我们要删除一个数,我们就要把他所有的前缀都cnt[p]--,插入和删除都可以通过insert函数来实现,大功告成,代码实现如下
代码
#include<iostream>
#include<algorithm>
using namespace std;
const int N=100010*32;
int son[N][2],cnt[N],s[N];
int idx;
void insert(int x,int v)
{
int p=0;
for(int i=31;i>=0;i--)
{ int u=x>>i&1;
if(!son[p][u])
{
son[p][u]=++idx;
}
p=son[p][u];
cnt[p]+=v;
}
}
int query(int x)
{
int res=0;
int p=0;
for(int i=31;i>=0;i--)
{
int u=x>>i&1;
if(cnt[son[p][!u]])
{
res=res*2+1;
p=son[p][!u];
}
else
{
res=res*2+0;
p=son[p][u];
}
}
return res;
}
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
{
int x;
scanf("%d",&x);
s[i]=s[i-1]^x;
}
insert(0,1); //插入0,以方便 第一个数^自己是他本身
int ans=0;
for(int i=1;i<=n;i++)
{
if(i>m)
{
insert(s[i-m-1],-1);
}
ans=max(query(s[i]),ans);
insert(s[i],1);
}
cout<<ans;
}
最大异或路径
输入样例:
4
0 1 3
1 2 4
1 3 6
输出样例:
7
样例解释
样例中最长异或值路径应为 0->1->2,值为 7(=3⊕4)
**定义f[i]为我定义的根节点(0)到其他点的异或和
将起点0放入dfs,并将到每一个点的异或路径值存到f中
则任意两点a,b的异或路径和 为 f[0,a]^f[0,b]
最大异或路径就可以套上最大异或对的板子了,将所有f[i]插入Trie树中,在从0-m遍历query一下就找到答案了
**
[//]: # (打卡模板,上面预览按钮可以展示预览效果 ^^)
//这里填你的代码^^
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1e5+5;
int tol, n, m, f[N], head[N], idx;
int son[N*32][2];
//链式前向星存树
struct node
{
int to,cost,next;
}edge[N*2];
void add(int u, int v, int w)
{
edge[tol].to = v;
edge[tol].cost = w;
edge[tol].next = head[u];
head[u] = tol++;
}
//最大异或对板子
void insert(int x)
{
int p = 0;
for(int i = 30; i >= 0; i --)
{
int u = x >> i & 1;
if(!son[p][u])
son[p][u] = ++idx;
p = son[p][u];
}
}
int query(int x)
{
int p = 0;
int ans = 0;
for(int i = 30; i >= 0; i --)
{
int u = x >> i & 1;
if(son[p][!u])
{ans = ans * 2 + 1;
p = son[p][!u];
}
else
{
ans = ans * 2 + 0;
p = son[p][u];
}
}
return ans;
}
//树的dfs
void dfs(int u,int father,int sum)
{
f[u] = sum;
for(int i = head[u]; i != -1; i = edge[i].next)
{
node e = edge[i];
if(e.to != father)
dfs(e.to, u , sum^e.cost);
}
}
int main()
{
cin >> m;
memset(head,-1,sizeof(head));
for(int i = 1; i <= m-1; i ++)
{
int u,v,w;
cin >> u >> v >> w;
add(u,v,w);
add(v,u,w);
}
dfs(0,-1,0);
for(int i = 1; i <= m; i ++)
{
insert(f[i]);
}
int ans = 0;
//查一遍就得到答案了
for(int i = 0; i <= m; i ++)
{
ans = max ( ans , query(f[i]));
}
cout << ans;
}
//注意代码要放在两组三个点之间,才可以正确显示代码高亮哦~