【字典树】笔记

震惊!超级大鸽子写帖子了!这究竟是人性的扭曲还是道德的沦丧?

麻了,为什么大佬们都去写难题题解了呀QAQ,就我一个小蒟蒻在新手村玩

Tips:该帖子属于蒟蒻向,所以大佬就可以尽情踩爆这个蒟蒻和水帖

1. 字典树

字典树(Trie)是一个较为独立的知识点,其最基本的操作可以插入和查询字符串

字典树常常运用于字符串相关的题目或者二进制相关的题目中

1.1. 字典树的基本操作

主要有两个,插入(insert)操作和查询(search)操作

1.1.1. 插入操作

在正式介绍插入操作前,我们先明白一个问题:

当我们把字符串存储在字典树中时,每一个字符是存储在边上的,结点所存储的是一些其他的信息

了解了这个后,我们正式介绍插入操作

我们会将字符串插入叫做 Trie 的二维数组中(Trie[i][j]表示 i i i 号结点指向 j j j 字符所对应的结点),具体过程如下:

  1. 依次遍历字符串 s s s 的每一个结点(现假设遍历到第 i i i 个字符,当前的结点编号为 P P P
  2. Trie[P][s[i]]为空,则设Trie[P][s[i]]为一个新的结点,然后将 P P P 设为所建立的新结点
  3. Trie[P][s[i]]为不空,则执行 P=Trie[P][s[i]]
  4. 遍历结束后,将 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. 字符串相关例题

Eg_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. 二进制相关问题

Eg_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 kdeep[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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值