Trie 学习笔记

Trie

\(\text{Trie}\) 树大家当然都很熟悉啦。

图源 OI Wiki/字符串/字典树

例如上面这个图,\(1\to 4\to 8\to 13\) 就代表字符串 cab

如果要查询这些字符串中是否存在 cba,就可以从根节点开始走,第一步往 c 代表的子节点处走,如果能走完就说明存在 cba。实际上一直这样走下去会走出 \(1\to 4\to 9\to 14\),因此存在 cba

这样可能会导致插入 cbad 但是查询是否存在 cba 也会认为存在,不过没关系,只需要在每个节点上打个标记表示它是不是某个字符串的末尾就可以了。

例如,如果上图中 \(5,11,7,12,15,13,14,10\) 号节点有标记,那么代表的字符串集合就是:

aa
aba
ba
caa
caaa
cab
cba
cc

\(\text{Trie}\) 树所需要的空间大小是所有字符串长度之和的线性函数。

具体地,设字符串的长度之和为 \(V\),字符集大小为 \(x\),那么所需空间就是 \(V\times x\)

此外,如果字符串的最大长度为 \(d\),那么 \(\text{Trie}\) 树的高度不会超过 \(d\),从根往下走一条链的复杂度就是 \(O(d)\)

01-Trie

\(\text{01-Trie}\) 指的就是字符集为 \(\{0,1\}\)\(\text{Trie}\)

由于一个数 \(x\) 在二进制下其实可以被看做一个 \(O(\log x)\) 位的 \(01\) 串,因此 \(\text{01-Trie}\) 可以被用来处理一些位运算相关的题目。这个 \(\text{Trie}\) 树的高度只有 \(O(\log V)\),这点性质非常好。

例如,要从 \(n\) 个数 \(a_1,\cdots,a_n\) 中选出两个数 \(a_i,a_j\) 使得 \(a_i\oplus a_j\) 最大(其中 \(\oplus\) 为异或),那么就可以将它们看做 \(n\) 个二进制 \(01\) 串并插入一个 \(\text{Trie}\)。枚举 \(a_i\),在 \(\text{Trie}\) 上从根往下走,由于我们肯定想让尽可能高的位尽量不同(这样就能在更高的位上产生 \(1\) 得到更大的答案),因此每次尽可能往与 \(a_i\) 的这一位不同的方向走就行了。

例题选讲

LuoguP2922 USACO08DEC Secret Message G Past 3

题意相当于对于给定的字符串序列,求出有多少字符串是它的前缀以及它是多少字符串的前缀。

对于第一部分,我们考虑对 \(\text{Trie}\) 树上每个节点记录一下 \(\text{Count}(u)\) 表示 \(u\) 是多少个单词的结尾,那么答案就是这个字符串对应路径上所有点的 \(\text{Count}(u)\) 之和。

对于第二部分,我们记录一下 \(\text{Size}(u)\) 表示 \(u\) 子树内有多少个字符串的结尾,我们对于要查询的字符串顺着该字符串对应路径上一直走,如果走不完说明答案为 \(0\),否则答案就是末尾节点的 \(\text{Size}\) 值。

为了避免存在和查询的字符串相等的字符串导致重复统计,我们可以在计算第一部分的 \(\text{Count}\) 之和时不统计末尾结点的 \(\text{Count}\) 值,而在第二部分正常统计。这样每个字符串就只会被统计一次了。AC Code

UVA11488 Hyper Prefix Sets Past 4

注意到 \(\text{Trie}\) 树上的一条路径 \(\text{root}\to u\) 就代表了 \(u\) 子树内所有字符串的一个公共前缀。

因此建出 \(\text{Trie}\) 树,记录一下每个节点的 \(\text{Size}\),然后答案就是 \(\text{max}_u\{\text{Dep}(u)\cdot \text{Size}(u)\}\),其中 \(\text{Dep}\) 表示深度,也就是根节点到这里的字符串长度。AC Code

UVA1401 Remember the Word Past 5

考虑一个朴素的 dp:设 \(F(i)\)\(S[1\cdots i]\) 这个前缀的分割方案数,那么

\[F(i)=\sum_{j|S[j+1\cdots i]\in D}F(j) \]

其中 \(D\) 表示给出的字符串集合。

直接暴力 dp 当然不行,我们考虑优化。

考虑枚举 \(D\) 中的字符串 \(T\),如果 \(T\)\(S[1\cdots i]\) 的一个后缀,那么就可以算上贡献 \(F(i-|T|)\)

这个转移不太方便,我们换一种定义方式:设 \(F(i)\)\(S[i\cdots n]\) 这一后缀的分割方案数,那么转移方程就是

\[F(i)=\sum_{T\in D,T\text{ is a prefix of }S[i\cdots n]}F(i+|T|) \]

\(\text{prefix}\) 表示前缀。

\(D\) 中的所有字符插入一棵 \(\text{Trie}\) 树,同时记录每个节点是不是某个字符串的末尾结点;将 \(S[i\cdots n]\) 这个字符串在 \(\text{Trie}\) 树上走一遍,如果找到一个末尾结点就代表找到了一个合法的 \(T\),进而计算贡献。

时间复杂度为 \(O(nL)\),其中 \(L=100\) 表示字符串的最大长度,也就是 \(\text{Trie}\) 树的高度。AC Code

然而其实这题完全没必要用 \(\text{Trie}\) 做,直接从 \(i\) 枚举 \(100\) 个然后算一下字符串 \(\text{Hash}\) 就行了。。。。

(我写到这里才意识到这一点 QAQ)

其实我们仔细想一下,这里 \(\text{Trie}\) 的作用其实就类似一个字符串哈希,就是查询字符串集合 \(D\) 里面是否存在 \(S[i\cdots j]\) 这个字符串。所以本质其实是一样的!

LuoguP4683 IOI2008 Type Printer Future 6

我第一眼看到这个题直接开始想 dp 然后就寄了=_= 看了题解才会orz

将所有字符串插入一个 \(\text{Trie}\),那么一个方案就是对这棵树进行一次 \(\text{DFS}\) 得到的 \(\text{DFS}\)

但特殊的是我们 \(\text{DFS}\) 完最后一个叶子结点之后不需要再回溯了,那么就能省下来这一条链长。

因此每次优先走较短的链,把最长的那条留在最后走就行了。时间复杂度是线性的。很妙!

AC Code

LuoguP4551 最长异或路径 Past 4

这题也配紫?

\(d_u\)\(1\to u\) 路径上边权异或和,那么可以发现 \(u\to v\) 路径上边权异或和就等于 \(d_u\oplus d_v\)

为什么呢,其实由于 \(w\oplus w=0\),因此 \(1\to \text{LCA}\) 路径上边权被算了两次就消掉了,最后算进去的只有 \(u\to v\) 路径上的边。

那么问题就变成了之前说的,从 \(n\) 个数中选出两个数使得异或值最大。用 \(\text{01-Trie}\) 做就行了。AC Code

LuoguP4592 TJOI2018 异或 Present 5

首先树上问题可以求出来 \(\text{DFS}\) 序,然后对于子树内查询就是 \(\text{DFS}\) 序上一段连续的区间,链上查询可以直接树剖一下转化成 \(O(\log n)\) 段连续区间合并起来。

现在只需要考虑以下的问题:

  • 有一个序列,你需要回答 \(O(q\log n)\) 次查询
  • 每次查询给出 \(l,r,x\),需要求出序列的区间 \([l,r]\) 内的数中与 \(x\) 异或的最大值。

如果没有 \([l,r]\) 的限制那么可以发现和上题一模一样。

考虑将询问离线下来,按照右端点排序,这样就可以消掉 \(r\) 的那一维了。现在只需要处理 \(l\)

我们可以在 \(\text{01-Trie}\) 的每个节点上维护一个 \(\text{Maxpos}(u)\) 表示 \(u\) 子树内节点中在序列中位置的最大值。如果要查询和 \(x\) 异或的最大值,对于 \(x\) 的第 \(i\) 位(设其为 \(c\)),如果 c^1\(\text{Maxpos}\)\(\ge l\),那么就往 c^1 的方向走,同时令 ans|=(1<<i);否则就往 \(c\) 的方向走。

于是就做完了。时间复杂度 \(O((n+q)\log n\log V)\),其中 \(V=2^{30}\) 为值域。AC Code

LuoguP5283 十二省联考 2019 异或粽子 Present 6

\(S_i=a_1\oplus a_2\oplus\cdots\oplus a_i\),那么可以发现 \([l,r]\) 区间内的异或和就是 \(S_{l-1}\oplus S_r\)。证明与上题类似。

那么我们就是要求出前 \(k\) 大的 \(S_i\oplus S_j\) 的和,其中 \(i<j\)

我们考虑对每个 \(i\) 算出满足 \(j>i\)\(S_j\oplus S_i\) 最大的这个 \(j\),并将 \(S_j\oplus S_i\) 扔进一个大根堆。

每次我们从中取出最大的一个,将 \(S_i\oplus S_j\) 的值累计进答案。如果这个 \(S_i\oplus S_j\)\(i\) 是第 \(x\) 次取出,那么我们只需要找出第 \(x+1\) 大的 \(S_i\oplus S_j\) 的值,然后将其扔进大根堆就行了。

如何找出第 \(x\) 大的 \(S_i\oplus S_j\) 的值?将所有 \(S_i\) 插入一个 \(\text{01-Trie}\),记录一下每个点的 \(\text{Size}\)。每次查询时从根开始往下走,根据 \(x\) 与左右子树中与 \(S_i\) 这一位不同的那边的 \(\text{Size}\) 的大小关系判断往哪边走就行了。

然而 \(i<j\) 这个条件让我们很难做,因为对于每个 \(S_i\) 我们 \(\text{Trie}\) 中应该存的集合是不同的......

而且这里没办法直接使用上一题的 trick......就算在每个节点上维护一下子树内节点位置的最值,那么我们要处理的信息就会是一个类似于「子树内满足位置 \(\le i\) 的数的个数」,是一个二维数点,不是很好搞。

其实这个很好解决:注意到异或有交换律,因此直接找 \(2k\) 个价值最大的,然后除以二就行了。

时间复杂度 \(O(n\log V+k\log n\log V)\)。有点卡常,不过 \(3.50\text{s}\) 还是可以的。AC Code

利用此题中「查询第 \(k\) 大」的 trick,不难想到:

  • 可以使用 \(\text{01-Trie}\) 来实现平衡树的各种操作。
  • 复杂度 \(O(n\log V)\),并且常数很小!

Gym102331B Bitwise Xor Present 5

半年前 qbxt 讲的题我现在才懂,我好菜啊QAQ

貌似最近一场 CF 里有一个和这题几乎完全一致的题

有一个结论是:若干个数两两异或的最小值必然在排序后相邻两数处取到。

这是因为,要想让两个数异或的结果尽可能小,那就要让高位上尽可能相同。因此两数必然在排序后相邻位置。

因此先将序列排序,然后考虑 dp:设 \(F(i)\) 为以 \(i\) 为结尾的子序列个数,那么转移方程就是

\[F(i)=1+\sum_{j<i,a_i\oplus a_j\ge x}F(j) \]

可以使用 \(\text{01-Trie}\) 优化这个 dp。

具体来说,我们每次转移的时候就从根开始往下走,让前面的位都恰好等于 \(x\) 的前面几位,然后考虑这一位上能否选 \(0/1\)

  • \(x\) 这一位是 \(0\),那么我们可以直接加上不同方向那棵子树的答案,然后向另一边递归;
  • 否则必须往不同的方向走,从而使这一位是 \(1\),才能满足 \(\ge x\) 的条件。

时间复杂度 \(O(n\log V)\)AC Code

LuoguP7717 EZEC-10 序列 Future 7.0

首先条件可以转化为 \(a_{y_i}=a_{x_i}\oplus z_i\),因此 \(a_{x_i}\) 一旦确定了,那么 \(a_{y_i}\) 同样也就确定了。

我们考虑连边:对于每条限制 \((x_i,y_i,z_i)\),我们在 \(x_i,y_i\) 间连双向边,边权为 \(z_i\)

那么对于一个连通块,只要一个数确定了,所有数就都确定了。只需要计算每个连通块的答案然后乘起来就行了。

我一开始以为答案就是 \(k+1\),但是思考之后发现,尽管 \(a_{x_i}\le k,z_i\le k\)\(a_{x_i}\oplus z_i\) 的值仍然可能 \(> k\)

因此问题就变为:对于每个连通块,随便选一个点 \(x\),你需要求出有多少个值是「合法」的,即:对于每条 \(x\to p\) 的链,链上的边权异或和与 \(a_x\) 的取值异或得到的值不能超过 \(k\)

直接枚举 \([0,k]\) 内的所有取值,复杂度 \(O(nk)\)。这就是我半年前的做法。

实际上可以注意到,对于一个点 \(p\),对于任意一条 \(x\to p\) 的路径,其边权异或和必然相同;否则答案即为 \(0\)

我们可以对每个点 \(p\) 求出 \(x\to p\) 路径上的边权异或和,然后把这些数全都放进一个 \(\text{01-Trie}\) 里。

现在就相当于要求出:有多少个数,使得它与 \(\text{Trie}\) 内的所有数中异或最大值不超过 \(k\)

我们从根开始往下走,依次决定每一位应该选什么。

取到第 \(i\) 位时记录此时异或最大值 \(S\),然后讨论这一位上能否选 \(0/1\)

  • 如果 \(S>k\),那么直接递归终止,然后 return 回去。

  • 当前节点有两个子节点:那么不管现在取什么都必然会出现一个 \(1\)。因此直接令 \(S\) 加上 \(2^i\),往左右子树递归即可。

  • 当前节点有一个子节点:那么如果在这一位上选了相同的数,\(S\) 至多加上 \(2^i-1\)。如果 \(S+2^i-1\le k\) 那么肯定可以,因此可以直接将答案加上 \(2^{i-1}\),同时将 \(S\) 加上 \(2^i\)(相当于这一位上取了相反的数),继续递归;否则这一位上必然不能取不同的值,那么直接往相同方向递归即可。

  • 当前节点是叶子结点:那么如果 \(S\le k\) 就返回 \(1\),否则为 \(0\)

这样一来,我们相当于只遍历了一次 \(\text{Trie}\),总的复杂度就是 \(O(n\log V)\)。其中 \(V=2^{30}\) 为值域。AC Code

习题

字符串相关的 \(\text{Trie}\) 树:

\(\text{01-Trie}\)

posted @ 2022-03-25 14:36  云浅知处  阅读(204)  评论(0编辑  收藏  举报