Trie树简单应用

Trie树简单应用

首先,Trie的思想很容易理解,一张图解释一切:

image-20230111082959293

也即:字符集有多大,则开多少倍空间。

在实现上,我们用边来存储字符,然后开一个数组表示当前节点是否是一个字符串的结尾即可。

#include<bits/stdc++.h>
using namespace std;
#define N 1050500 
int t[N][26]={0},ed[N]={0},n,m,cnt=1;
char c[N];
void insert(char a[],int len){
	int p=1;
	for(int i=1;i<=len;i++){
		int x=a[i]-'a';
		if(!t[p][x])t[p][x]=++cnt;
		p=t[p][x]; 
	}
	ed[p]++;
}
int query(char a[],int len){
	int p=1,ans=0;
	for(int i=1;i<=len;i++){
		int x=a[i]-'a';
		if(!t[p][x])return ans;
		p=t[p][x];
		ans+=ed[p];
	}
	return ans;
}
int main(){
	ios::sync_with_stdio(false);
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>c+1;
		insert(c,strlen(c+1));
	}
	while(m--){
		cin>>c+1;
		cout<<query(c,strlen(c+1))<<endl;
	}
}

具体在于应用:

字符串方面

为了叙述方便,我们称某个节点表示的字符串即为根到此节点路径上的字符组成的字符串。

Trie树在插入的时候,其实根到两个叶子节点的LCA就为这两个叶子节点所代表的字符串的最长公共前缀,运用这个性质,我们可以解决很多问题。

几个常见的模型:

  1. 开一个cnt数组,在插入的时候顺便计数,即可得到当前这个节点表示的字符串是多少个节点的前缀。前缀统计
  2. 开一个fa数组记录父亲节点,即可在一个节点不断向上跳,跳到第一个有结束标记的节点即为这个节点所表示的字符串所存在的最大前缀。单词化简
  3. 将所有存在结束标记的节点单独抽离,建立一颗虚树,这个树高一般很低,所以可以在这棵树上搞很多操作,打标记之类的非常方便,而且可以很轻松地统计前缀的各种信息(类比树形DP)。例如背单词:贪心+trie重构树

Trie树还可以统计很多信息,主要关于前缀,如果是后缀,可以将每一个字符串reverse一下。

Trie树的综合题经常几结合贪心,DP等算法进行考察。

异或方面

Trie树有一个变种,我们称其为01Trie。它的思想是将每一个数二进制化,从高位到低位插入Trie中,由此进行统计。

注意的是,由于是从高位到低位,我们需要统一位数(一般取31,61之类)。

图解:

image-20230111084821004

实现插入的模板代码:

void insert(int x){
	int p=1;
	for(int i=30;i>=0;--i){
		int s=(x>>i)&1;
		if(!t[p][s])t[p][s]=++cnt;
		p=t[p][s];
	}
}

01trie在实际应用中更为广泛,我们来探讨几个常见模型:

异或最大值问题

问题简述:给定n个数a1,a2,,an,求maxi=1,j=1naiaj

解法:我们先将i1个数插入,然后将ai类似插入的过程,设当前已经算到第k位,指针为p,则将p赋值为t[p][((a[i]>>k)&1)^1)]

如果没有这个儿子,就只能走另一边。可以在这个过程中统计贡献,也可以在最后找到是哪一个数再进行计算。

这个解法成立是因为i=0k2k<2k+1

模板代码:

int find(int x){
	int p=1,ans=0;
	for(int i=30;i>=0;--i){
		int s=(x>>i)&1;
		if(t[p][s^1]){
			ans+=1<<i;
			p=t[p][s^1];
		}
		else {
			if(!t[p][s])return ans;
			p=t[p][s];
		}
	} 
	return ans;
}

这个问题有一些变种,类似于:

  1. 树上异或最短路径:预处理每个数到根路径上的异或和disi,然后dis就为上文的a,问题得到转化
  2. 各种最大子段和问题,可以类比各类一般的最大子段和进行解决,主要也参考异或前缀/后缀和的思路,对于一般最大子段和的做法,欢迎查阅拙作最大子段和及其常见扩展

异或带限制计数问题

问题类似:给定a1,a2,,an,求:i=1nj=i+1n[aiaj>k]

问题解决:

考虑先将所有的a插入,然后枚举ai,统计k=1n[akai>k]

对于这个做法,类比数位DP,我们设函数solve(s,t)表示计算k=1n[aks>t]

s,t的二进制表示分别为(bk,bk1b1),(ck,ck1c1)

对于答案的计算,可以将其分为两部分:可以在当前位解决的和留在后续解决的。

则我们设指针pk遍历到1,按位统计贡献,将这个过程看作我们不断填数的过程,设所填数为x,其二进制表示为(dk,dk1d1)

分类讨论:

  1. bp=1,cp=1。此时,我们若想要保持xs>t的可能,就需要让xs的第p位为1,也即我们填上dp=0,此时我们无法计算出答案
  2. bp=0,cp=1,类比情况一,填上dp=1
  3. bp=1,cp=0,此时我们将dp填为1/0,当填为0时,第p11位无论填什么都可以对答案产生贡献,所以直接累加上siz[t[p][0]]的贡献并将dp填为1即可。
  4. bp=0,cp=0,类比情况三,可以得到算上siz[t[p][1]]的贡献然后将dp填为0即可。

核心代码:

int solve(int s,int tt){//在trie中查找与s异或比t大的数的数量 
	int p=1,ans=0;
	for(int i=60;i>=0;i--){
		if(!p)return ans;
		int x=(s>>i)&1;
		if((tt>>i)&1){
			p=t[p][x^1];
		}
		else{
			ans+=siz[t[p][x^1]];
			p=t[p][x];
		}
	}
	return ans;
}

总结套路

对于01Trie的各种问题,都可以类比DP/贪心进行思考。一般来讲,我们都是枚举操作序列里的ai,快速地统计ai的贡献。很多时候需要综合题目信息,明确查询时需要的策略,善于分类讨论(没儿子,有一个儿子,有两个儿子)进行求解。时间复杂度一般为O(nlogn)

posted @   spdarkle  阅读(29)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示