【学习笔记】trie的进阶

Trie的进阶

前言

Trie名为“字典树”,在部分书中被归类于字符串板块。事实上,trie的功能并不局限于字符串的操作(字典的功能也不局限于查找单词嘛),而是有数据结构的功能。本文将探讨一些trie可解决的问题,主要包括有01trie,可持久化trie和例题三部分。

这玩意不是论文

本文内容基本完工,未来可能会持续更新。

Latest Updated:20210310 可持久化部分编写完毕
20210305 可持久化trie试写完毕,准备开始完善
20210224 新增例题 20201018 新增例题

Vol 1 01trie

01trie是一种比较强大的异或问题解决工具。常有人云:关于异或的题呢,我们一般会有两种想法:整形异或线性基,01Trie。

01trie构建方法是:将每个数转为统一位数的二进制数,然后按实际需求选择正/倒着建trie。01trie对于解决Xor(异或)类问题有很大意义。

首先是Xor的常用式子,这些东西在后面会有用,建议记忆。

0^0=0 0^1=1 1^1=0

a^a=0 0^a=a

首先上例题HDU 4825 Xor Sum

本题要求从给出的集合中选一个数\(K\)使得该数与输入的\(S\)的异或最大。这便是01trie的模板题。

要使异或最大,则应尽量让靠前的二进制位为1,也就是若\(S\)为1,尽量选0,反之亦然。如给出的\(S\)为5 \((101)_2\),则经过0 1 1 显然是最优解。借助trie,上述贪心可以较快实现。

依照上文所述的方法构建trie。如样例为3 4 5,则我们构建的trie应为

     0
     |(省略若干0)
   /   \
  0      1
  |      |
  1      0
  |     / \
  1    0   1

可以看出,此处选择正着建树是为了满足靠前的二进制位为1,补0的作用是不会因对应位数不同产生混乱(如\((111)_2\)\((11)_2\))。

上代码

void maketree(int x,long long y,int z)
{
	if (z==-1)
	{
		tr[x].e=y;
		return ;
	}
	long long nu=y&mi[z];	
	if (nu)
	{
		if (tr[x].n1) 
		  maketree(tr[x].n1,y,z-1);
		else
		{
			cc++;
			tr[x].n1=cc;
			maketree(cc,y,z-1);
		}
	}
	else
	{
		if (tr[x].n0) 
		  maketree(tr[x].n0,y,z-1);
		else
		{
			cc++;		
			tr[x].n0=cc;
			maketree(cc,y,z-1);
		}
	}
	return ;
}//建trie,记得补位
void ask(int x,long long y,int z)
{
	if (z==-1)
	{
		cout<<tr[x].e<<endl;
		return ;
	}
	long long nu=y&mi[z];
	if (nu)
	{
		if (!tr[x].n0)
		{
			if (!tr[x].n1) return ;
			  else ask(tr[x].n1,y,z-1);
		} 
		else
		{
			ask(tr[x].n0,y,z-1);
		}
	}
	else
	{
		if (!tr[x].n1)
		{
			if (!tr[x].n0) return ;
			  else ask(tr[x].n0,y,z-1);
		} 
		else
		{
			ask(tr[x].n1,y,z-1);
		}
	}//尽量使该位为1
}

这便是01trie基本结构。

再来两道例题:

Luogu P4551 最长异或路径

本题要求在边有权值的树上求两点使得路径的异或之和最大。点数达到\(10^5\)

显然可以以两点LCA为界将路径一分为二。那么我们处理任意一点向上到根(可指定任意一点为根)的经过的每一点的异或即可。显然时间复杂度过大。

根据\(a\ xor\ a=0\),同一条边经过两次对异或和不会产生任何影响。因此我们仅处理任意一点到根的节点即可。那么问题将变为在\(n\)个处理出的数中选两个使得异或值最大。建01trie后,采用对每个数匹配最大值的方法即可。时间复杂度为\(O(n)\)常数稍大。

CodeForces 817E Choosing The Commander

本题要求维护一个可重集合,支持加入,删除和查询有多少个元素异或上 \(p\)小于 \(l\)的操作。操作最大为100000。

维护一个01trie,在trie中插入和删除的操作想必不用多说。关键在于查询。

我们注意到题目给出的条件已给定异或的一个参数以及结果,不难反推出另一参数。向该参数遍历,若结果的当前位为1,则两参数异或为0的情况一定合法,则依此统计答案即可。

Vol 2 题目选讲

这里涉及trie的一些做法巧妙的题目,欢迎向笔者提供更多好题。因为篇幅需要,以下将只提供简略题解。如需详细题解

[IOI2008] Type Printer 打印机

题意:一个打印机,支持向其加减一个字母和打印的操作。给出\(N\)个单词,求打印这些单词的最小操作数。注意,打印结束后允许打印机内有字母,且打印次序任意。

\(1\le N \le 25000\),单词均小写,最大长度20

题解:显然若两单词有相同前缀,则可以通过共用前缀的方式解决。这样可减少一次加减共两个操作。根据这一条件,我们联想到结构与此一致的trie,故使用Trie维护单词。

[省选联考 2020 A 卷] 树

题目大意:

题解:考虑使用dfs,在每个点维护一个trie,对于每个节点,先与子节点的trie合并,然后按位统计该位\(0/1\)个数,并在向上更新答案时进行\(+1\)的操作,得出答案。

CodeForces 888G Xor-MST

题意:给定 \(n\) 个结点的无向完全图。每个点有一个点权为 \(a_i\) 。连接 \(i\) 号结点和 \(j\) 号结点的边的边权为 \(a_i\oplus a_j\)
求这个图的 MST 的权值。
\(1≤n≤2×10^5 ,0\le a_i< 2^{30}\) ,时限2S

题解:完全图考虑用Boruvka算法解决MST问题。在Trie上,有 \(n\) 个不同的数,则意味着\(n-1\) 个点会同时有左右儿子。联通块合并则是对于这些同时拥有左右儿子的节点的左右儿子合并。trie在本题起优化作用。

Vol 3 可持久化

好的,在这篇blog发出7个多月后,同时也是上一次可持久化写了将近3年后,po终于AC了可持久化trie的模板题。

考虑到可能有人从这里开始接触可持久化,我们先来介绍一下可持久化。

可持久化是与数据结构结合的,如:可持久化线段树/trie/平衡树。它的显著特点是保留每一个历史版本,因此可以解决一些普通数据结构所无法解决的问题。版本回溯便是一类可持久化数据结构可解决的问题。

回到trie。为了保留历史版本,如果直接复制旧版本再修改,则空间必然会爆炸。考虑到一次修改所改变的东西并不多,我们可以每次只修改被添加或值被修改的节点,而保留没有被改动的节点,在上一个版本的基础上连边(PS:可持久化线段树也是如此)。这样,我们在可完整访问每一版本的基础上节省了空间。

因为 01trie+可持久化 出题的思路比较广,所以,一般可持久化trie是以01trie的形式出现。

是的,这样就讲完了。其实可持久化线段树&字典树扩充的东西不多。

下面上例题Luogu P4735 最大异或和

题意:给一个序列,支持两种操作:在序列末尾加一个数;找到一个位置 \(p\),满足 \(l \le p \le r\),使得: \(a[p] \oplus a[p+1] \oplus ... \oplus a[N] \oplus x\) 最大,并求最大值
\(N,M\le300000\)

这个数据范围大概是要求数据结构维护的。维护xor一般有线性基和01trie,故我们考虑01trie。

首先,我们将问题转化一下:设 \(s[i]\)\(a[1] \oplus a[2]\oplus ... \oplus a[i]\) ,则 \(a[p] \oplus a[p+1] \oplus ... \oplus a[N]=s[p-1] \oplus s[N]\),于是,我们可以通过前缀和 寻找最大的 \(s[p]\)

我们无法避免的问题是需要区间的操作。考虑使用可持久化。把\(1 \le i \le r\) 的trie复制出来(注意:并不是真正复制,这样会导致超时。访问该版本即可),这样我们可以去除掉 \(r\) 的限制,至于 \(l\) 的限制,通过打标记即可解决。剩下的就是与上文一样的给定 \(x\) ,从给出的集合中选一个数 \(K\) 使异或最大的问题了。

上代码

struct trie
{
	int a[2],tmp,lst;
}t[61001000];
int q,q1,r[600100],a,s[600100],n,m,L,R,x,tmpp,num;char ch;
void addtrie(int r,int ro,int y)
{
	if (y==-1) 
	{t[r].lst=num;return ;}
	if (tmpp&(1<<y))
	{
		if (t[ro].a[0]) t[r].a[0]=t[ro].a[0];
		q++;t[r].a[1]=q;t[q].tmp=tmpp;
		addtrie(t[r].a[1],t[ro].a[1],y-1);
	}
	else
	{
		if (t[ro].a[1]) t[r].a[1]=t[ro].a[1];
		q++;t[r].a[0]=q;t[q].tmp=tmpp;
		addtrie(t[r].a[0],t[ro].a[0],y-1);
	}
	t[r].lst=num;
}
int findmin(int x,int dep,int ans)
{
	if (dep==-1) return ans;
	int tmp=int(bool(tmpp&(1<<dep)));
	if (t[x].a[tmp^1]&&t[t[x].a[tmp^1]].lst>=L) 
	  return findmin(t[x].a[tmp^1],dep-1,ans|(1<<dep));
	else
	{
	 	if (t[t[x].a[tmp]].lst>=L) return findmin(t[x].a[tmp],dep-1,ans);
	 	else return 2147483647;
	}
}

AC记录1.83S&183.72MB 还是比较优秀的。

Vol 4 涉及可持久化的题目选讲

后记

感谢_Wallace_的题单

本文部分参考OI-wiki、部分题解、博客及《算法竞赛进阶指南》

posted @ 2021-03-08 21:45  fmj_123  阅读(322)  评论(0编辑  收藏  举报