首页 > 其他分享 >Trie树简单应用

Trie树简单应用

时间:2023-01-11 09:11:06浏览次数:61  
标签:前缀 Trie ans int 异或 应用 简单 节点

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\)个数\(a_1,a_2,…,a_n\),求\(\max_{i=1,j=1}^na_i\oplus a_j\)

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

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

这个解法成立是因为\(\sum_{i=0}^k2^k<2^{k+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. 树上异或最短路径:预处理每个数到根路径上的异或和\(dis_i\),然后\(dis\)就为上文的\(a\),问题得到转化
  2. 各种最大子段和问题,可以类比各类一般的最大子段和进行解决,主要也参考异或前缀/后缀和的思路,对于一般最大子段和的做法,欢迎查阅拙作最大子段和及其常见扩展

异或带限制计数问题

问题类似:给定\(a_1,a_2,…,a_n\),求:\(\sum_{i=1}^{n}\sum_{j=i+1}^n[a_i\oplus a_j>k]\)

问题解决:

考虑先将所有的\(a\)插入,然后枚举\(a_i\),统计\(\sum_{k=1}^n[a_k\oplus a_i>k]\)

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

设\(s,t\)的二进制表示分别为\((b_k,b_{k-1}…b_1),(c_k,c_{k-1}…c_1)\)

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

则我们设指针\(p\)从\(k\)遍历到\(1\),按位统计贡献,将这个过程看作我们不断填数的过程,设所填数为\(x\),其二进制表示为\((d_k,d_{k-1}…d_1)\)

分类讨论:

  1. \(b_p=1,c_p=1\)。此时,我们若想要保持\(x\oplus s>t\)的可能,就需要让\(x\oplus s\)的第\(p\)位为1,也即我们填上\(d_p=0\),此时我们无法计算出答案
  2. \(b_p=0,c_p=1\),类比情况一,填上\(d_p=1\)
  3. \(b_p=1,c_p=0\),此时我们将\(d_p\)填为\(1/0\),当填为\(0\)时,第\(p-1\sim 1\)位无论填什么都可以对答案产生贡献,所以直接累加上\(siz[t[p][0]]\)的贡献并将\(d_p\)填为\(1\)即可。
  4. \(b_p=0,c_p=0\),类比情况三,可以得到算上\(siz[t[p][1]]\)的贡献然后将\(d_p\)填为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/贪心进行思考。一般来讲,我们都是枚举操作序列里的\(a_i\),快速地统计\(a_i\)的贡献。很多时候需要综合题目信息,明确查询时需要的策略,善于分类讨论(没儿子,有一个儿子,有两个儿子)进行求解。时间复杂度一般为\(O(n\log n)\)。

标签:前缀,Trie,ans,int,异或,应用,简单,节点
From: https://www.cnblogs.com/oierpyt/p/17042805.html

相关文章