Trie 字典树
【Trie 树】
我们试图给每一个字符串一个对应的值。但是由于字符串长度不定,也不是数字,所以不能用数组。
有几种办法:
-
STL map,二叉排序树(平衡树),
。 为字符串长度。 -
Trie,插入删除查询都是
的。但是对空间有要求,每一个位置上可能的字符不能太多。 -
哈希。
我们现在讲讲 Trie。
Trie 是一个树形结构。一开始我们有一个根结点,这个根结点不代表任何字符。在插入字符串的过程中,我们维护这棵树。
每插入一个字符串,我们从根结点出发,如果当前结点有一个对应当前字符的儿子,就去往这个儿子并处理下一个字符;如果没有,就新建一个对应字符的儿子,再去往这个儿子。
等处理完所有字符,把所在结点标记为
Trie 的时间复杂度很低
但是为了保证 Trie 的时间复杂度是稳定在
于是我们一般在每个结点上都设立一个儿子数组。如果没有对应字符的儿子,可设为一个特殊数,如
这样父节点可以直接调用数组查询儿子编号。但是这样也导致了我们空间开销大,即使没有对应字符的儿子,也会开一个数组空间。
如果每个位置上的字符选择太多,空间就会炸掉。
我们可以把 Trie 的每个结点写成结构体,再把 Trie 再写成结构体。用 vector 存结点们。这样可以避免空间浪费太多,虽然常数大一点。(也可以开个全局的 Trie,用一次清一次,常数小点)
我们可以在每个结点上记录一个 pfx:表示这个结点子树内所有结点
函数传参用引用传参可以节省时间。
Trie 的优势是快,可以做一部分的字符串,逐步处理,map 和哈希只能处理完整的字符串;缺点是空间大。
【题目们】
互为前缀:走过的路径上
前缀、末尾信息记录
A 是 B 的前缀,B 是 C 的前缀。那在查找到 C 时一定已经走过了 A 和 B。
在 Trie 上搜索,到达叶子结点时比大小。
Trie 上搜索
dp 很好想:
注意题目有条件单词长度不超过
问题是如何快速判断
Trie 优化 dp
给每个人开一个 Trie。
把所有电话号翻转,再存进对应的 Trie 里。
如果一个结点的
翻转 => 后缀,
把
Trie 上搜索,
如果
问题来了:这个算法正确,但是复杂度看起来很高。每次查找一个单词最坏可能搜索整颗 Trie。
整条搜索路径看起来像是:一条主路(和原单词相同的路径)分出来很多条分路(一个位置不同,之后全相同)。
我们换个角度计算复杂度:考虑一个结点会贡献多少复杂度。
如果一个结点下方出去
【Trie 和二进制编码】
我们可以把数化为二进制编码,把二进制编码视作字符串,建成一颗 Trie。这个 Trie 称为 0-1 Trie。当然,最好把所有二进制编码都做成相等长度。
一般在按位的操作下,才会用 01 Trie。
我们对所有可乐编号建立 01 Trie,在 Trie 上走,得出每个可能的
假设
如果选了
我们递归地进入选/不选
然后就到了选不选
如果我们不选
故所有在右子树结尾的数可乐都可以喝到,直接调用右子结点的
而左子树,因为我们不选
这是一个同类型但更简单的子问题:
递归终点:如果当前结点为
综上,在每一个结点选择向左和向右的情况都讨论了,只要把两种情况递归求出再取
↑ 运用性质:如果能不在当前结点产生贡献,就一定不要。因为二进制导致了之后所有可能贡献加起来都没有这个结点大。
按位处理,递归
相当经典
part 0
倍增 LCA 预处理出每个点的 LCA 和倍增的异或值。
然后
part 1
注意到两个相等的数异或起来为
所以两个结点的路径边权异或,等于这两个点到根结点的路径边权异或 再异或。从 LCA 到根结点的部分异或两次消去了。
求出每个点到根结点的路径异或值。
有一个很基本的想法:
这省掉了 LCA 的复杂度。
part 2
我们无法接受
考虑只枚举一个结点,如何快速求出谁和它异或最大?
我们把所有结点到根结点的路径异或值化为二进制,建成 01Trie。
当我们枚举到结点
如果当前位上是 且 Trie 上的当前结点有 的儿子,就必须去这个儿子。因为二进制有性质:之后所有数加起来都比不过这个儿子异或产生的贡献。
复杂度优化到
↑ 运用性质:1. 异或消去律; 2. 如果能不在当前结点产生贡献,就一定要。因为二进制导致了之后所有可能贡献加起来都没有这个结点大。
Trie 快速求一个数
按位处理,贪心
维护一个数据结构,支持:
-
插入数;
-
删除数;
-
给定一个数,查询这个数的最大异或。
插入和查询都在之前讲过,而删除实际上可以用类似插入的手法,把要删除的数在 Trie 上走一遍,所过之处
搜索时进入新结点,先判断它的
选出异或和最大的
首先根据消去律,可以用俩前缀异或。
把前缀们建成 01 Trie 树。
发现 01 Trie 上都是左边大,右边小,所以可以用类似二叉排序树二分
经典想法:如何取出很多很多数中的前
记
二分求出异或第
-
插入新数;
-
选一个数
,使 和 的异或最大。
难点在于如何保证
朴素算法
每个叶子结点对应一个数,每个数有一个编号。
在每个结点上记录一个
假设当我们在一个结点,
这可以用 lower_bound
求出:如果
优化:离线算法
调整问题顺序,当我们加入一个数,
这样我们在结点上就不用记录
【练习】
把所有数都插入,然后搜索,如果一条路径上有至少两个
字符串版 LIS。
首先规则一肯定不会使用。
规则二可以视作规则三的
那我们就只看规则三好了。
part 1
后缀不好处理,字符串翻转变前缀,存 Trie 树里面。
我们在插入的时候额外在字符串结尾的结点记录一下这是几号单词的结尾。
part 2
然后我们可以用一次搜索得知谁是谁的后缀,并且建出一张图:
我们断言:把每个点的子结点列表按照子结点子树大小从小到大排序,然后从根结点开始搜索,得到的 dfs 序就是使代价最小的排列顺序。
part 3
证明:
0)一个显然的结论:当一个结点(一个单词)被选之前,它的父结点(后缀)一定已经被选了。 (重点!思路也由此切入)
1) 先证明一定是按照 dfs 序。
如果不是按照 dfs 序来,考虑一个结点下属有若干子树,其中一颗子树有叶子结点
(这里能保证
考虑此时总花费的变化,把
花费增多。
2)再证明一定是按照子树大小从小到大最优。
因为考虑一个结点的所有儿子。第
所以肯定是规模越小越靠前。
字符串翻转,建 Trie 树,变成前缀相同的押韵。
我们观察一番,应该要找到一颗包含
-
根结点之外的结点都是
; -
所有结点的儿子至多只能有一个不是
。
树形 dp。
其实就是跑一次 Trie 上搜索,但还有一点注意:
因为打印完最后可以留东西,所以最长的单词应该最后遍历,然后不用删除。
那我们可以在建完 Trie 后再取出最长的单词,用类似插入的方法给这个单词的每个结点打标记。搜索时优先去没标记的结点。
枚举每个字符串为最小的,那么说明第
最后在建出来的图上跑拓扑排序,看看是否有环。有环就不行。
当然如果有其他字符串是前缀也不行。
一道水题 只要学过 Trie 就能写出来
因为单词的长度很短,所以我们可以直接枚举经过一次替换、删除、添加后所有可能的字符串,然后直接在 Trie 里查找是否存在。
注意因为可能有重复,所以每次查询再开一个 Trie 存下来所有被查询过的字符串。
但是我们需要判重,如果用 map,自带
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!