【学习笔记】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基本结构。
再来两道例题:
本题要求在边有权值的树上求两点使得路径的异或之和最大。点数达到\(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 涉及可持久化的题目选讲
后记
本文部分参考OI-wiki、部分题解、博客及《算法竞赛进阶指南》