简要说明

  • \(Trie\) 树在多个串的检索中十分实用,也是自动机的基础.
  • 它可以将若干个字符串组合成一颗树,不少题可以建好 \(Trie\) 树后在上面树形 \(dp\) .
  • 另外一个应用是解决最大异或值的问题,将数看成 \(01\) 串,贪心地在 \(Trie\) 树上走.

题目

Phone List

  • 题意:给若干数字串,问有没有两个串满足一个串是另一个串的前缀.
  • 将读入的串一个个插入 \(Trie\) 树中,不难发现串 \(A\) 是串 \(B\) 的前缀,只有两种情况.
  • 一种情况是串 \(A\) 先被插入,那么在插入串 \(B\) 时,一定会经过 \(A\) 的单词节点.
  • 另一种情况是串 \(B\) 先被插入,那么在插入串 \(A\) 时,都是沿着 \(B\) 的边走的,并没有新建边.
  • 一边插入,一边判断这两种情况是否出现即可.
  • 另一个简单而有效的做法是\(hash\).将出现过的前缀全部用 \(vis\) 数组标记即可.
View code

#include"bits/stdc++.h"
using namespace std;
typedef long long LoveLive;
inline int read()
{
	int out=0,fh=1;
	char jp=getchar();
	while ((jp>'9'||jp<'0')&&jp!='-')
		jp=getchar();
	if (jp=='-')
		{
			fh=-1;
			jp=getchar();
		}
	while (jp>='0'&&jp<='9')
		{
			out=out*10+jp-'0';
			jp=getchar();
		}
	return out*fh;
}
const int MAXN=5e5+10;
const int Siz=15;
int tcnt=0;
struct Trie{
	inline int idx(char c)
		{
			return c-'0';
		}
	int ch[MAXN][Siz];
	int val[MAXN];
	int cnt;
	void init()
		{
			memset(ch,0,sizeof ch);
			cnt=0;
		}
	int ins(char *s)
		{
			int n=strlen(s);
			int u=0;
			int f=0;
			for(int i=0;i;++i)>

The XOR Largest Pair

  • 题意:给若干个整数,求出两两异或能得到的最大值.
  • 使用 \(Trie\) 树,将每个数看做一个长度为 \(32\) 的字符串插入 \(Trie\) 树中.
  • 插入完毕后,枚举一个数,在 \(Trie\) 树中往下走,尽量使每步的 \(0/1\) 和枚举的数都不一样.
  • 这样贪心取即可求出答案.
View code

#include"bits/stdc++.h"
using namespace std;
typedef long long LoveLive;
inline int read()
{
	int out=0,fh=1;
	char jp=getchar();
	while ((jp>'9'||jp<'0')&&jp!='-')
		jp=getchar();
	if (jp=='-')
		{
			fh=-1;
			jp=getchar();
		}
	while (jp>='0'&&jp<='9')
		{
			out=out*10+jp-'0';
			jp=getchar();
		}
	return out*fh;
}
const int MAXN=1e6+10;
const int MAXL=31;
struct Trie{
	int idx;
	int ch[10*MAXN][2];
	Trie()
		{
			idx=0;
			memset(ch,0,sizeof ch);
		}
	void ins(int x)
		{
			int u=0;
			for(int i=MAXL;i>=0;--i)
				{
					int k=(x>>i)&1;
					if(!ch[u][k])
						ch[u][k]=++idx;
					u=ch[u][k];
				}
		}
	int maxxor(int x)
		{
			int u=0;
			int res=0;
			for(int i=MAXL;i>=0;--i)
				{
					int k=(x>>i)&1;
					if(ch[u][k^1])
						{
							res<<=1;
							res+=1;
							u=ch[u][k^1];		
						}
					else
						{
							res<<=1;
							u=ch[u][k];
						}	
				}
			return res;
		}
}T;
int a[MAXN];
int main()
{
	int n=read();
	for(int i=1;i<=n;++i)
		{
			a[i]=read();
			T.ins(a[i]);
		}
	int ans=0;
	for(int i=1;i<=n;++i)
		ans=max(ans,T.maxxor(a[i]));
	printf("%d\n",ans);
	return 0;
}

Nikitosh 和异或

  • 题意:给出 \(n\) 个整数,选出两个严格不相交的区间 \(A=[l_1,r_1],B=[l_2,r_2]\) ,使得 \(A\) 的异或和加上 \(B\) 的异或和最大,求出这个最大值.
  • 若记录前缀异或和 \(s\) ,根据异或的性质,\(x \ xor\ x =0,\)\(a[l_1]\ xor\ a[l_1+1]\ xor \ ......a[r_1]=s[r_1]\ xor \ s[l_1-1].\)
  • 可以记\(l[i]\)表示在\(i\)左侧选出区间的最大异或值,\(r[i]\)表示在\(i\)右侧选出区间的最大异或值.
  • 将前/后缀依次插入 \(Trie\) 中,找出最大异或和即可.
View code

#include"bits/stdc++.h"
using namespace std;
typedef long long LoveLive;
inline int read()
{
	int out=0,fh=1;
	char jp=getchar();
	while ((jp>'9'||jp<'0')&&jp!='-')
		jp=getchar();
	if (jp=='-')
		{
			fh=-1;
			jp=getchar();
		}
	while (jp>='0'&&jp<='9')
		{
			out=out*10+jp-'0';
			jp=getchar();
		}
	return out*fh;
}
const int MAXN=1e6+10;
const int MAXL=31;
struct Trie{
	int idx;
	int ch[10*MAXN][2];
	void ins(int x)
		{
			int u=0;
			for(int i=MAXL;i>=0;--i)
				{
					int k=(x>>i)&1;
					if(!ch[u][k])
						ch[u][k]=++idx;
					u=ch[u][k];
				}
		}
	int maxxor(int x)
		{
			int u=0;
			int res=0;
			for(int i=MAXL;i>=0;--i)
				{
					int k=(x>>i)&1;
					if(ch[u][k^1])
						{
							res<<=1;
							res+=1;
							u=ch[u][k^1];		
						}
					else
						{
							res<<=1;
							u=ch[u][k];
						}	
				}
			return res;
		}
	void init()
		{
			idx=0;
			memset(ch,0,sizeof ch);
			ins(0);//避免1个数求不出的情况 
		}
	Trie()
		{
			init();
		}
}T;
int a[MAXN],n;
int l[MAXN],r[MAXN];//i两侧的最大值 
int main()
{
	n=read();
	int cur=0;
	for(int i=1;i<=n;++i)
		{
			a[i]=read();
			cur^=a[i];
			T.ins(cur);
			l[i]=max(l[i-1],T.maxxor(cur));
		}
	cur=0;
	T.init();
	for(int i=n;i>=1;--i)
		{
			cur^=a[i];
			T.ins(cur);
			r[i]=max(r[i+1],T.maxxor(cur));
		}
	int ans=0;
	for(int i=1;i;++i)>

L 语言

  • 题意:给出若干个单词和若干篇文章.对于每篇文章,求出它能被完全匹配的最大前缀长度.
  • 将单词全部插入 \(Trie\) 树中.用\(f[i]\)表示前缀\(1\)\(i\) 能否被单词完全匹配.
  • 匹配文章时,应先连好失配边(所以这其实是一个 \(AC\) 自动机的题对吧).
  • 若当前匹配到了单词结点,且这个单词的开头的前面都能被完全匹配,显然这个位置的前缀可以被完全匹配,更新答案.
  • 在插入单词的时候将权值 \(val\) 设置为单词长度记录下来即可.
View code

#include"bits/stdc++.h"
using namespace std;
typedef long long LoveLive;
inline int read()
{
	int out=0,fh=1;
	char jp=getchar();
	while ((jp>'9'||jp<'0')&&jp!='-')
		jp=getchar();
	if (jp=='-')
		{
			fh=-1;
			jp=getchar();
		}
	while (jp>='0'&&jp<='9')
		{
			out=out*10+jp-'0';
			jp=getchar();
		}
	return out*fh;
}
const int MAXN=1e6+10;
const int Siz=26;
char buf[MAXN];
struct Trie{
	int ch[1000][Siz];
	int val[1000];
	int fail[MAXN];
	int idx;
	int f[MAXN];
	Trie()
		{
			memset(ch,0,sizeof ch);
			memset(fail,0,sizeof fail);
			idx=0;
		}
	void ins(char *s)
		{
			int n=strlen(s);
			int u=0;
			for(int i=0;i q;
			q.push(0);
			while(!q.empty())	
				{
					int u=q.front();
					q.pop();
					for(int i=0;i;++i)>;++i)>

Secret Messages

  • 题意:给出 \(N\) 个信息串, \(M\) 个密码串.对于每个密码串,回答有几个信息串与它满足一个是另一个的前缀.
  • 将信息串插入 \(Trie\) 树中,记录子树大小.询问时,是密码串前缀的,在搜索过程中会经过,密码串是它的前缀的,到最后一个字符处加上子树大小即可.
View code

#include"bits/stdc++.h"
using namespace std;
typedef long long LoveLive;
inline int read()
{
	int out=0,fh=1;
	char jp=getchar();
	while ((jp>'9'||jp<'0')&&jp!='-')
		jp=getchar();
	if (jp=='-')
		{
			fh=-1;
			jp=getchar();
		}
	while (jp>='0'&&jp<='9')
		{
			out=out*10+jp-'0';
			jp=getchar();
		}
	return out*fh;
}
const int MAXN=5e5+10;
struct Trie{
	int idx;
	int ch[MAXN][2];
	int siz[MAXN];//计算自身的子树大小 
	int val[MAXN];
	Trie()
		{
			idx=0;
			memset(ch,0,sizeof ch);
			memset(siz,0,sizeof siz);
			memset(val,0,sizeof val);
		}
	void ins(int n)
		{
			int u=0;
			for(int i=1;i<=n;++i)
				{
					int k=read();
					if(!ch[u][k])
						ch[u][k]=++idx;
					u=ch[u][k];
				}
			++val[u];
		}
	void dfs(int u)
		{
			siz[u]=val[u];
			for(int i=0;i<=1;++i)
				if(ch[u][i])
					{
						dfs(ch[u][i]);
						siz[u]+=siz[ch[u][i]];
					}
		}
	int solve(int n)
		{
			int u=0,res=0,flag=0;
			for(int i=1;i<=n;++i)
				{
					int k=read();
					if(!ch[u][k])
						flag=1;
					if(flag)
						continue;
					u=ch[u][k];
					if(i!=n)
						res+=val[u];
					else
						res+=siz[u];
				}
			return res;
		}
}T;
int main()
{
	int n=read(),m=read();
	for(int i=1;i<=n;++i)
		{
			int len=read();
			T.ins(len);			
		}
	T.dfs(0);
	for(int i=1;i<=m;++i)
		{
			int len=read();
			int ans=T.solve(len);
			printf("%d\n",ans);
		}
	return 0;
}

背单词

  • 题意:给你 \(n\) 个字符串,不同的排列有不同的代价,代价按照如下方式计算(字符串 \(s\) 的位置为 \(x\) ):

    • 1.排在 \(s\) 后面的字符串有 \(s\) 的后缀,则代价为$ n^2$ ;

    • 2.排在 \(s\) 前面的字符串有 \(s\) 的后缀,且没有排在 \(s\) 后面的 \(s\) 的后缀,则代价为 \(x-y\)\(y\) 为最后一个与 \(s\) 不相等的后缀的位置);

    • 3.\(s\) 没有后缀,则代价为\(x\)

    求最小代价和。

  • 将所有字符串翻转插入一颗 \(Trie\) 树中,则关于后缀的问题全部转化为前缀.

  • 容易发现,我们一定可以避免 \(1\) 情况的出现( \(DAG\) 图拓扑排序),且避免后代价一定更优.最坏也只有\(\frac{n(n+1)}{2}\ < \ n^2\).若在位置 \(0\) 加上一个虚拟根作为所有字符串的前缀,那么情况 \(3\) 可以看做是 \(2\) 的特殊情况,可以一起处理.

  • 将每个字符串作为一个节点,给节点编号,求节点编号与父亲编号差的最小值即可.

  • 贪心地做, \(dfs\) 按照子树大小从小到大选出来先编号即可.

View code

#include"bits/stdc++.h"
using namespace std;
typedef long long LoveLive;
inline int read()
{
	int out=0,fh=1;
	char jp=getchar();
	while ((jp>'9'||jp<'0')&&jp!='-')
		jp=getchar();
	if (jp=='-')
		{
			fh=-1;
			jp=getchar();
		}
	while (jp>='0'&&jp<='9')
		{
			out=out*10+jp-'0';
			jp=getchar();
		}
	return out*fh;
}
const int MAXL=510010;
const int Siz=26;
const int MAXN=1e5+10;
int tot=0;
int cnt=0,head[MAXN],to[MAXN<<1],nx[MAXN<<1];
inline void add(int u,int v)
{
	++cnt;
	to[cnt]=v;
	nx[cnt]=head[u];
	head[u]=cnt;
}
struct Trie{
	int idx;
	int ch[MAXL][Siz];
	int val[MAXL];
	Trie()
		{
			idx=0;
			memset(ch,0,sizeof ch);
			memset(val,0,sizeof val);
		}
	void ins(char buf[],int n,int v)
		{
			int u=0;
			for(int i=n-1;i>=0;--i)
				{
					int k=buf[i]-'a';
					if(!ch[u][k])
						ch[u][k]=++idx;
					u=ch[u][k];
				}
			val[u]=v;
		}
	void build_graph(int u,int pre)
		{
			++tot;
			if(val[u])
				add(pre,val[u]),pre=val[u];
			for(int i=0;i pii;
pii tmp[MAXN];
stack stk;
void solve(int u,int fa)
{
	int pos=0;
	vis[u]=++tot;
	ans+=vis[u]-vis[fa];
	for(int i=head[u];i;i=nx[i])
		{
			int v=to[i];
			tmp[++pos]=make_pair(-siz[v],v);
		}
	sort(tmp+1,tmp+1+pos);
	for(int i=1;i<=pos;++i)
		stk.push(tmp[i].second);
	while(pos--)
		{
			int v=stk.top();
			stk.pop();
			solve(v,u);
		}
}
int main()
{
	int n=read();
	for(int i=1;i<=n;++i)
		{
			scanf("%s",buf);
			T.ins(buf,strlen(buf),i);			
		}
	T.build_graph(0,0);
	dfs(0);
	solve(0,0);
	printf("%lld\n",ans);
	return 0;
}
;++i)>

The XOR-longest Path

  • 题意:给一颗边权树,求出树上最长的异或和路径.
  • 利用异或的优秀性质,可以处理出节点 \(1\) 到每个点的距离 \(dis\) ,那么 \(u\)\(v\) 之间的异或和距离直接就是 \(dis[u]\) ^ \(dis[v]\) .被重复计算的部分自身异或两次抵消了.
  • 那么将 \(dis\) 数组求出后,问题就变为在这个数组中找两个数,使得这对数异或值最大.
  • 使用 \(Trie\) 树的经典做法解决即可.
View code

#include"bits/stdc++.h"
using namespace std;
typedef long long LoveLive;
inline int read()
{
	int out=0,fh=1;
	char jp=getchar();
	while ((jp>'9'||jp<'0')&&jp!='-')
		jp=getchar();
	if (jp=='-')
		{
			fh=-1;
			jp=getchar();
		}
	while (jp>='0'&&jp<='9')
		{
			out=out*10+jp-'0';
			jp=getchar();
		}
	return out*fh;
}
const int MAXN=1e5+10;
int head[MAXN],nx[MAXN<<1],to[MAXN<<1],val[MAXN<<1],cnt=0;
inline void add(int u,int v,int w)
{
	++cnt;
	nx[cnt]=head[u];
	to[cnt]=v;
	val[cnt]=w;
	head[u]=cnt;
}
int n;
int dis[MAXN];
struct Trie{
	int idx;
	int ch[MAXN*10][2];
	Trie()
		{
			memset(ch,0,sizeof ch);
			idx=0;
		}
	void ins(int x)
		{
			int u=0;
			for(int i=31;i>=0;--i)
				{
					int k=(x>>i)&1;
					if(!ch[u][k])
						ch[u][k]=++idx;
					u=ch[u][k];
				}
		}
	int maxxor(int x)
		{
			int u=0,res=0;
			for(int i=31;i>=0;--i)
				{
					int k=(x>>i)&1;
					if(ch[u][k^1])
						{
							res<<=1;
							res+=1;
							u=ch[u][k^1];
						}
					else
						{
							res<<=1;
							u=ch[u][k];
						}
				}
			return res;
		}
}T;
void dfs(int u,int fa)
{
	for(int i=head[u];i;i=nx[i])
		{
			int v=to[i];
			if(v==fa)
				continue;
			dis[v]=dis[u]^val[i];
			dfs(v,u);
		}
}
int main()
{
	n=read();
	for(int i=1;i;++i)>

小结

  • 将数字按照二进制位插入 \(Trie\) 树形成的东西应该是叫 \(01\ Trie\) 树...这个东东可以用来解决异或有关问题,还可以做到可持久化来解决区间问题(形态不改变,类似于主席树).若维护出节点 \(siz\) 后,可以解决平衡树的一类问题.涉及范围比较广泛,但大多还是只在解决异或问题时使用,因为其他时候我们可以使用我们更熟悉的数据结构.
  • 可持久化的 \(Trie\) 树实现咕了以后来补.
  • \(Trie\) 树的可持久化一般也只用于解决数字问题.

相关文章:

  • 2022-12-23
  • 2021-07-20
  • 2022-03-03
  • 2022-03-10
  • 2022-01-07
  • 2021-06-20
  • 2022-12-23
  • 2022-12-23
猜你喜欢
  • 2021-11-24
  • 2021-06-28
  • 2022-12-23
  • 2021-08-02
  • 2022-12-23
  • 2021-12-15
相关资源
相似解决方案