20260813 - 字典树 总结

前言

严肃发现复制粘贴代码十分浪费时间,我以后不在总结里贴代码了。

在四场比赛中分别获得了一三二四的排名,祝贺!(这对吗

我明天该不会是第五名了吧 /jk

原来 J26 没教过字典树吗?原来 J26 没教过字典树吗?原来 J26 没教过字典树吗?原来 J26 没教过字典树吗?原来 J26 没教过字典树吗?这不对吧???

我不会 Trie,Trie 好难啊。

llk 开桂了,哇啊啊啊。

写 Trie 时应该思考什么

  1. Trie 中的一个节点代表什么?
    • 代表一个前缀的最后一个字母,其为父节点的哪个字母(或数字)的儿子,则对应了该节点代表的字母。
  2. 从根到一个节点的路径代表什么?
    • 存入树中的某个(也可能是某些)字符串的前缀(不一定是真前缀)。
  3. 一个节点的子树代表哪个字符串集合?
    • 代表以根到该节点的路径所代表的前缀为前缀的字符串集合。
  4. cnt[u] 和 ed[u] 有什么区别?
    • 前者为经过这个点的字符串个数,后者为以这个点为结尾的字符串个数。
  5. 为什么把整数写成二进制后,也可以建立 Trie?
    • 因为写成二进制之后也可以看做字符串、字符串的前缀去考虑。
  6. 什么题适合 Trie,什么题不适合?
    • 很粗略的总结就是,一些涉及到“前缀”或“包含某个字符串”的题目常常会用到 Trie。以下的题目也可以证明这点,所以我就不详讲“为什么这题要用到 Trie”这类问题了。

回答很潦草啊,将就看吧 /kel。

接下来讲题。

A - trie模板

枚举每篇短文,将其所有单词塞进 Trie 里边,然后一一枚举每个单词并查找 Trie 中是否存在该单词完整版本即可。

B - Karuta

要找的就是与指定字符串的前缀交集最大的字符串。诶,这个好办,先建 Trie,然后考虑从每个字符串的终点(也就是最后一个字符所在的节点)出发往上探,探到的第一个 \(cnt_u > 1\) 的 \(u\) 节点的深度就是这个字符串的答案。至于为什么是判断 \(cnt_u > 1\) 而不是 \(>0\),是因为经过 \(u\) 点的至少会有当前字符串一个呀。

C - Secret Message G

前缀长度必须等于两个字符串中较短的长度,那么就有两种情况:如果消息长度比暗号要短,那么暗号去遍历 Trie 时就把前缀的 \(ed\) 数组累加起来;如果消息长度比暗号要长,那么暗号去遍历 Trie 时就把暗号结尾的 \(cnt\) 数组加进去。总结下来就是,在经过加结尾,在结尾加经过。

D - Yet Another Sigma Problem

枚举到当前字符串时,先把前面的字符串建好 Trie,然后以当前字符串为基准遍历 Trie,并累加一路上经过的位置所对应的 \(cnt\) 数组,因为我们要算的是前缀的长度,所以累加经过数组是正确的。然后,再把当前字符串也扔进 Trie,为下一个字符串做准备即可。

E - 01trie模板

首先,两个节点之间的异或距离等于它们到根的异或距离的异或值,你可能要问了诶 \(\text{LCA}\) 上面的值不应该算啊,注意这里是异或呢,异或两次会抵消的。

那么就可以快速求出每个节点到根的异或距离,建出 01-Trie,然后枚举每个节点的权值去处理,为了使异或结果最大,我们应该尽可能选择走与当前值相反的那条道路。一定会有路走,不需要考虑路断了的情况,因为拆成二进制后大家的位数是统一的。

F - Perfect Security

这就更是简单题了啊,因为 \(a\) 的顺序固定而 \(p\) 的顺序不固定,所以一一考虑 \(a\) 值。每次(利用和上一题一样的技巧)找出能使 \(a_i \oplus p_j\) 最小的 \(p_j\) 并使用(因为题目要求字典序最小),然后把这个 \(p_j\) 删掉,防止之后再用。删除可以直接删掉 \(cnt\) 和 \(ed\) 的记录,扫的时候注意一下即可,不需要真的把一个节点删掉,这样很麻烦的呀。

G - Forbidden Prefix

有点难度的题啊,不少人似在了 test 34 上,不知道是什么原因。我一遍过的,虽然写了挺久。

对 \(X\) 和 \(Y\) 分别维护一棵 Trie。我们的理想实现是,当前字典树内的 \(Y\) 值全部都是还没有前缀与 \(X\) 匹配的 \(Y\) 值,而剩下的 \(Y\) 值均被删掉。当发现部分 \(Y\) 值被某个 \(X\) 值干掉时,答案减去这一部分,并把这些 \(Y\) 值从字典树里面删掉。

考虑增加 \(X\) 和 \(Y\) 时的操作。

增加 \(X\) 时,我们首先将 \(X\) 放进 \(X\) 的 Trie 里面,然后用这个 \(X\) 值遍历 \(Y\) 的 Trie。如果最后跑到了 \(X\) 的终点并且该节点存在 \(cnt\),说明有 \(cnt\) 个 \(Y\) 值被干掉了,答案变化。问题在于如何删掉这些 \(Y\),这是比较复杂也比较难想的一部分,大概就是首先这个节点下面的子树的 \(cnt\) 值全部清空,然后这个节点往上的所有祖先节点的 \(cnt\) 都要减去当前节点的(原)\(cnt\) 值。

增加 \(Y\) 时,先不要把 \(Y\) 放进 Trie(需要先判断有没有被 \(X\) 一上来就干掉),先用这个 \(Y\) 值遍历 \(X\) 的 Trie。如果在跑的过程中某个节点在 \(X\) 那儿有 \(ed\),说明这个 \(Y\) 的前缀是这个 \(X\),那么这个 \(Y\) 就非法(不需要加进 Trie 了)。如果跑到最后依然合法,就把这个 \(Y\) 值加入 \(Y\) 的 Trie 里面,并变化答案。

实现有点史啊,还是有很多细节的。

H - Edit to Match

水水的题,没有放在 H 的理由。

考虑到 \(S_k\) 时,先把 \(S_1,S_2,\dots,S_{k-1}\) 全部存好在 Trie 里面了。然后遍历 \(S_k\) 的状况,到的这个位置可以理解为 LCA,找另外一个点(\(ed\) 所在位置)离这个的最近距离。这个可以在插入的时候维护一个 \(mn\) 数组实现,那么就做完了,需要一丢丢推式子算一下(大概就是一个 \(|S_k| - i -1\) 因为是 \(0\)-based),很简单啦。

I - Dr. Evil Underscores

上课的时候没做出来的原因是因为没去想(?,我选择了开 J,事实证明这是正确的。

一次 DFS 就行了。首先建出 Trie 这是不必多说了,紧接着我们从根节点出发遍历这棵 Trie 树。到一个节点时,如果它是叶子那么直接返回 \(0\),如果它只有一个儿子那也只有一条路往下走,答案也不会有变化(取当前点的值即可),如果它有两个儿子则答案注定要发生变化(可以先加上),但是此时你无法快速抉择哪个儿子更优。反正不会 TLE,那直接两个儿子各跑一通,取最小就好了。

J - A Lot of Games

好玩题。

注意到先手和后手在这里不一定只争夺“获胜”了,他们也可能同时想要“失败”,因为败家会是下一场的先手,这可能导致下一场获得更好的成绩。

建出 Trie,那么每个点都一定有一个必胜或必败态(在两人都争夺同一目标时)。叶节点一定是必败态,以“渴望获胜”举例,如果一个节点的所有儿子都是必胜态,那么这个节点被迫成为必败态,而如果一个节点有至少一个儿子是必败态,那么这个节点就一定是必胜态。原理很简单就不说了。

那么最后我们可以得到根节点在争夺两种不同目标时先手的状态。

  • 如果先手能控制局面(能赢能输),则先手一定必胜。
    • 因为先手可以让自己在前 \(k-1\) 轮都输,稳住自己的先手身份,并在最后一局获胜。
  • 如果先手无论怎么下一定都是赢的,那么 \(k\) 为奇数时先手赢,否则后手赢。
    • 此时先手身份将在两人之间跳来跳去,因为胜负必定不需要人为决定,所以决定胜负的是比赛场数啦。
  • 如果先手无论怎么下一定都是输的,则后手一定必胜。
    • 因为先手一直是输的,那也一直是先手,最后一轮先手也是输,后手就赢了。
  • 如果先手在想要赢的时候输,在想要输的时候赢,则后手一定必胜。
    • 因为此时先手的输赢完全由后手控制,所以后手一定可以控制自己在最后一局获胜。

做完了,特别有意思对吧(。

总结

字典树一般处理的都是“前缀”或“包含”的一些字符串匹配问题,总之还是挺有用的啦。哦对了,字典树开数组的时候一定要记得算算数组大小,开大了 MLE 开小了 RE 很头疼。

明天杂题能翻吗……?

感觉这篇总结还要潦草一点,但是四千多字了诶,这对吗 OvO。

posted @ 2026-08-13 19:41  嘎嘎喵  阅读(28)  评论(0)    收藏  举报