震惊!超级大鸽子写帖子了!这究竟是人性的扭曲还是道德的沦丧?
麻了,为什么大佬们都去写难题题解了呀QAQ,就我一个小蒟蒻在新手村玩
Tips:该帖子属于蒟蒻向,所以大佬就可以尽情踩爆这个蒟蒻和水帖
1. 字典树
字典树(Trie)是一个较为独立的知识点,其最基本的操作可以插入和查询字符串
字典树常常运用于字符串相关的题目或者二进制相关的题目中
1.1. 字典树的基本操作
主要有两个,插入(insert)操作和查询(search)操作
1.1.1. 插入操作
在正式介绍插入操作前,我们先明白一个问题:
当我们把字符串存储在字典树中时,每一个字符是存储在边上的,结点所存储的是一些其他的信息
了解了这个后,我们正式介绍插入操作
我们会将字符串插入叫做 Trie
的二维数组中(Trie[i][j]
表示
i
i
i 号结点指向
j
j
j 字符所对应的结点),具体过程如下:
- 依次遍历字符串 s s s 的每一个结点(现假设遍历到第 i i i 个字符,当前的结点编号为 P P P)
- 若
Trie[P][s[i]]
为空,则设Trie[P][s[i]]
为一个新的结点,然后将 P P P 设为所建立的新结点 - 若
Trie[P][s[i]]
为不空,则执行P=Trie[P][s[i]]
- 遍历结束后,将 P P P 当前所在的结点做一个结尾标记
我们通过图片来举个栗子
这是初始状态的字典树,现在我们插入字符串ab
首先,遍历第一个字符a
(此时
P
=
1
P=1
P=1)
然后,判断Trie[1]['a']
是否为空,显然,为空,所以将Trie[1]['a']
设为
2
2
2(
1
1
1 已经被占据了)
此时,字典树长这样:
同理,遍历第二个字符b
(此时
P
=
1
P=1
P=1)
判断Trie[2]['b']
是否为空,显然,为空,所以将Trie[2]['b']
设为
3
3
3
此时,字典树长这样:
由于该字符串已经插入完毕,所以在 P P P 号结点(即 3 3 3 好结点)做一个结尾标记
此时,字典树长这样:
接下来,考虑插入第二个字符串ac
首先,遍历第一个字符a
(此时
P
=
1
P=1
P=1)
然后,判断Trie[1]['a']
是否为空,不为空,所以将P
设为 Trie[1]['a']
(
1
1
1 已经被占据了)
此时,字典树未改变
同理,遍历第二个字符b
(此时
P
=
2
P=2
P=2)
判断Trie[2]['c']
是否为空,显然,为空,所以将Trie[2]['c']
设为
4
4
4
此时,字典树长这样:
由于该字符串已经插入完毕,所以在 P P P 号结点(即 4 4 4 好结点)做一个结尾标记
此时,字典树长这样:
至此,我们就梳理了字典树的插入操作,下面是代码时间:
void insert(char *str){
int len=strlen(str),tot=1; //确定字符串的长度以及初始结点的赋值
for(int i=0;i<len;i++){
int k=str[i]-'a'; //得到第i个字符串所对应的字符
if(!Trie[tot][k]){ //对应的所指向的结点不存在
Trie[tot][k]=++cnt; //造一个新结点
}
tot=Trie[tot][k]; //指向对应的结点
}
End[tot]=1; //对末尾结点做一个结尾标记
}
1.1.2. 查询操作
查询操作比较简单,假设我们要查询字符串 s s s,我们只需要按照插入那样一个一个的去寻找结点,如果找不到,说明字典树中没有这个字符串,那么我们就直接退出查询,如果找到了该字符串,就说明这个字符串曾经出现过。。。吗?
比如说,如果是这个所要查找的字符串是某一个字符串的前驱呢?
举个例子:
在这个例子中,我们只插入了一个字符串aa
,如果我们要查询a
这个字符串,显然是找得到的,但是它只是aa
这个字符串的前驱,并没有被插入啊?
此时,我们之前所标记的结尾标记就起作用了
当我们找到了该字符串的末尾结点 P P P 时,判断 P P P 结点是否有结尾标记,如果有,说明我们找到了这个字符串,如果没有,则说明这个字符串只是某个字符串的前缀而已
代码时间:
int search(char *str){
int len=strlen(str),tot=1; //确定字符串的长度以及初始结点的赋值
for(int i=0;i<len;i++){
int k=str[i]-'a'; //得到第i个字符串所对应的字符
if(!Trie[tot][k]){ //对应的所指向的结点不存在
return 0; //该单词不存在
}
tot=Trie[tot][k]; //指向对应的结点
}
return End[tot]; //返回是否有结尾标记,理由见上
}
至此,我们已经了解了字典树所有的基本操作,现在来介绍一下目前字典树的用法
1.2. 字典树的两大基本用法
上文已言,字典树常常运用于字符串相关的题目或者二进制相关的题目中
现在我们分别举一个例子:
1.2.1. 字符串相关例题
一句话题意:
有 n n n 个字符串 S S S 和 m m m 组询问,每一组询问给定一个字符串 s s s,求在这 n n n 个字符串中有多少个字符串满足 s s s 是 S i S_i Si 的前缀
这道题可以算是一道模板题
我们只需要修改字典树模板中的一个东西即可
在上文的字典树模板中,End[i]
的作用是标记
i
i
i 号结点是否有结尾标记
现在,我们重新定义End[i]
设从
1
1
1 号结点到
i
i
i 号结点所得到的字符串为
s
s
s(以查询操作所给出的图为例,从
1
1
1 号结点到
3
3
3 号结点所得到的字符串为aa
),则End[i]
表示有多少个字符串满足
s
s
s 是该字符串的前缀
举个例子:
在这样的一颗字典树中,End[1]=End[2]=2,End[3]=End[4]=1
那么,我们要怎样才能完成这个操作呢?
先上代码:
void insert(char *str){
int len=strlen(str),tot=1;
for(int i=0;i<len;i++){
int k=str[i]-'a';
if(!Trie[tot][k]){
Trie[tot][k]=++cnt;
}
tot=Trie[tot][k];
End[tot]++; //唯一的变动
}
}
原理是什么呢?
当一个字符串 s s s 在插入字典树后,会有一条从 1 1 1 号结点的路到某结点的路径,使得该路径所对应的字符串为 s s s
那么,任意选取该路径上的一点 i i i ,从 1 1 1 号结点的路到 i i i 结点的路径所对应的字符串必然是 s s s 的前缀
换言之,我们要将该路径上的所有点的标记全部累加 1 1 1,该操作在插入操作中即可完成
那么,这道题也就可以顺利的完成了
代码时间:
#include<cstdio>
#include<cstring>
int n,m,cnt=1;
char s[3000005];
int Trie[3000005][65];
int End[3000005];
int get(char x){ //由于字符串的组成成分有点复杂,所以要写一个函数,将每一个字符与其对应的数字对应起来
if(x>='A'&&x<='Z'){
return x-'A';
}else if(x>='a'&&x<='z'){
return x-'a'+26;
}else{
return x-'0'+52;
}
}
void insert(char *str){ //插入操作
int len=strlen(str),tot=1;
End[1]++;
for(int i=0;i<len;i++){
int k=get(str[i]);
if(!Trie[tot][k]){
Trie[tot][k]=++cnt;
}
tot=Trie[tot][k];
End[tot]++;
}
}
int search(char *str){ //查询操作
int len=strlen(str),tot=1;
for(int i=0;i<len;i++){
int k=get(str[i]);
if(!Trie[tot][k]){
return 0;
}
tot=Trie[tot][k];
}
return End[tot];
}
int main(){
int T;
scanf("%d",&T);
while(T--){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%s",s);
insert(s);
}
for(int i=1;i<=m;i++){
scanf("%s",s);
printf("%d\n",search(s));
}
for(int i=1;i<=cnt;i++){ //多组输入,别忘了初始化
for(int j=0;j<65;j++){
Trie[i][j]=0;
}
End[i]=0;
}
cnt=1;
}
return 0;
}
1.2.2. 二进制相关问题
一句话题意:
给定一个 n n n 个结点的带权树,求该树上最大的异或路径(异或路径指从结点 i i i 到结点 j j j 上的路径的所有边权的异或和)
首先,让我们想一想:如何求到一个异或路径
这个的原理其实和求结点 i i i 到结点 j j j 上的路径边权和原理类似
首先,假设
i
i
i 结点和
j
j
j 结点的 LCA 为
k
k
k,deep[i]
表示结点
1
1
1 到结点
i
i
i 的异或路径
显然,如果用*
表示异或(主要是真不知道异或的
LaTeX
\LaTeX
LATEX 怎么打)可以得到下述式子:
deep[ i ] = ( i , fa [ i ] ) ∗ ( fa [ i ] , fa [ fa [ i ] ] ) ∗ ⋯ ∗ ( son1 [ k ] , k ) ∗ ( k , fa [ k ] ) ∗ ⋯ ∗ ( son [ son [ 1 ] ] , son [ 1 ] ) ∗ ( son [ 1 ] , 1 ) \text{deep[\ i\ ]}=(i,\text{fa}[\ i\ ])^*(\text{fa}[\ i\ ],\text{fa}[\ \text{fa}[\ i\ ]\ ])^*\cdots^* (\text{son1}[\ k\ ],k)^*(k,\text{fa}[\ k\ ])^*\cdots^* (\text{son}[\ \text{son}[\ 1\ ]\ ],\text{son}[\ 1\ ])^*(\text{son}[\ 1\ ],1) deep[ i ]=(i,fa[ i ])∗(fa[ i ],fa[ fa[ i ] ])∗⋯∗(son1[ k ],k)∗(k,fa[ k ])∗⋯∗(son[ son[ 1 ] ],son[ 1 ])∗(son[ 1 ],1)
同理可得:
deep[ j ] = ( j , fa [ j ] ) ∗ ( fa [ j ] , fa [ fa [ j ] ] ) ∗ ⋯ ∗ ( son2 [ k ] , k ) ∗ ( k , fa [ k ] ) ∗ ⋯ ∗ ( son [ son [ 1 ] ] , son [ 1 ] ) ∗ ( son [ 1 ] , 1 ) \text{deep[\ j\ ]}=(j,\text{fa}[\ j\ ])^*(\text{fa}[\ j\ ],\text{fa}[\ \text{fa}[\ j\ ]\ ])^*\cdots^* (\text{son2}[\ k\ ],k)^*(k,\text{fa}[\ k\ ])^*\cdots^* (\text{son}[\ \text{son}[\ 1\ ]\ ],\text{son}[\ 1\ ])^*(\text{son}[\ 1\ ],1) deep[ j ]=(j,fa[ j ])∗(fa[ j ],fa[ fa[ j ] ])∗⋯∗(son2[ k ],k)∗(k,fa[ k ])∗⋯∗(son[ son[ 1 ] ],son[ 1 ])∗(son[ 1 ],1)
将两者进行异或,根据异或的定理, ( k , fa [ k ] ) (k,\text{fa}[\ k\ ]) (k,fa[ k ]) 之后的东西全部都可以省略掉,所以,得到下述式子:
deep[ i ] ∗ deep[ j ] = ( i , fa [ i ] ) ∗ ( fa [ i ] , fa [ fa [ i ] ] ) ∗ ⋯ ∗ ( son1 [ k ] , k ) ∗ ( son2 [ k ] , k ) ∗ ⋯ ∗ ( fa [ j ] , fa [ fa [ j ] ] ) ∗ ( j , fa [ j ] ) \text{deep[\ i\ ]}^*\text{deep[\ j\ ]}=(i,\text{fa}[\ i\ ])^*(\text{fa}[\ i\ ],\text{fa}[\ \text{fa}[\ i\ ]\ ])^*\cdots^* (\text{son1}[\ k\ ],k)^*(\text{son2}[\ k\ ],k)^*\cdots^*(\text{fa}[\ j\ ],\text{fa}[\ \text{fa}[\ j\ ]\ ])^* (j,\text{fa}[\ j\ ]) deep[ i ]∗deep[ j ]=(i,fa[ i ])∗(fa[ i ],fa[ fa[ i ] ])∗⋯∗(son1[ k ],k)∗(son2[ k ],k)∗⋯∗(fa[ j ],fa[ fa[ j ] ])∗(j,fa[ j ])
我们不然发现,这就是我们想要的异或路径
所以,我们只需要用一个dfs,求出所有的deep[i]
,然后就可以用
O
(
1
)
O(1)
O(1) 的时间复杂度求得一条异或路径了
其实,现在的问题已经简化成了:
给定 n n n 个整数,选择两个数,使它们的异或结果最大
因为异或与二进制有关,所以我们可以把每一个整数转化成二进制(即01串)
考虑:对于01串 s s s ,什么样的01串 t t t 与 s s s 异或后所得到的结果可以最大化?
显然,我们肯定希望对于每一位 i i i, s i ≠ t i s_i\ne t_i si=ti
那如果在考虑第 i i i 位的时候,找不到满足 s i ≠ t i s_i\ne t_i si=ti 的 t t t 串怎么办?
咱们就退而求其次,一样的又不是不可以
那么,我们在进行查询操作的时候进行一顿猛改,就可以轻易的做到上述的操作
那么,先给查询操作一个特写:
void search(char ch[],long long int kkk){ //kkk代表着所查询的20串代表的数值
int len=strlen(ch),tot=0;
long long int sum=0;
for(int i=0;i<len;i++){
int k=ch[i]-'0';
if(Trie[tot][(k+1)%2]){ //如果有满足s_i不等于t_i的01串t
tot=Trie[tot][(k+1)%2]; //那肯定往这边走
sum=sum*2+(k+1)%2; //处理当前所遍历到的t串所代表的数值
}else{ //没有满足s_i不等于t_i的01串t
tot=Trie[tot][k]; //退而求其次
sum=sum*2+k;
}
}
ans=max(sum^kkk,ans); //得到了对于当前s串而言的最优答案,更新答案
}
现在,代码时间:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=4000005;
long long int edge[N];
int ver[N],Next[N],head[N],len;
void add(int x,int y,long long int z){
ver[++len]=y,edge[len]=z,Next[len]=head[x],head[x]=len;
}
int n,cnt=1,x,y;
long long int z,ans;
long long int deep[N];
char ch[50];
int Trie[N][5];
void dfs(int x,int fa,long long int kkk){
deep[x]=(deep[fa]^kkk); //不难得到deep[x]=deep[fa[x]]^(x,fa[x])
for(int i=head[x];i;i=Next[i]){
int y=ver[i];
long long int z=edge[i];
if(y!=fa){ //要往下遍历
dfs(y,x,z);
}
}
}
void Do(long long int num){ //将十进制数转化为01串
int len=32; //要统一位数,不然插入字典树是就是乱的
while(len>=0){
ch[len]=num%2+'0';
len--;
num/=2;
}
}
void insert(char ch[]){ //插入操作
int len=strlen(ch),tot=0;
for(int i=0;i<len;i++){
int k=ch[i]-'0';
if(!Trie[tot][k]){
Trie[tot][k]=++cnt;
}
tot=Trie[tot][k];
}
}
void search(char ch[],long long int kkk){ //查询操作见上面的特写
int len=strlen(ch),tot=0;
long long int sum=0;
for(int i=0;i<len;i++){
int k=ch[i]-'0';
if(Trie[tot][(k+1)%2]){
tot=Trie[tot][(k+1)%2];
sum=sum*2+(k+1)%2;
}else{
tot=Trie[tot][k];
sum=sum*2+k;
}
}
ans=max(sum^kkk,ans);
}
int main(){
scanf("%d",&n);
for(int i=1;i<n;i++){
scanf("%d%d%lld",&x,&y,&z);
add(x,y,z);
add(y,x,z);
}
dfs(1,0,0); //计算每一个deep[i]
for(int i=1;i<=n;i++){
Do(deep[i]);
search(ch,deep[i]);
insert(ch);
}
printf("%lld",ans);
return 0;
}