Trie 字典树重温
详解
引入
为了解决一类字符串前缀问题,应该用什么数据结构快速支持?
详解
可以把多个字符串的同一前缀利用起来。如 abc,abcd,就只利用好 abc。
但是有若干个字符串,我们不妨把每个字母拆开,就能都利用了。比如 abc,abcd,a,就利用好 a,b,c,d。
如何利用?遍历这 4 个字母就能做到遍历 3 个字符串。考虑把这个字母拆成链的形式,方便遍历,即 a->b->c,若干条链即可上树,组成一颗 26 叉树。
类比二叉树的 ch[u][0/1],我们定 tr[u][x] 表示 u 结点往下一个结点指向第 x 个儿子。其中 x 就是字符串不同字符数量。
然后不断 insert,就完成建树。查询同理。
insert:当前结点能往下走我就走,发现没有结点给我走,我就创建一个结点。
query: 当前结点能往下走我就走,发现没有结点给我走,我就真的走不动了。
注意,由定义可知 tr 的第一维是结点数量。也就是是字母最大长度总和,而非字母最大长度
板子
int get(char c) { if (c>='a' && c<='z') return c-'a'+1; if (c>='0' && c<='9') return 26+c-'0'+1; return 36+c-'A'+1; } void insert(char *s) { int p=0, n=strlen(s+1); for (int i=1; i<=n; i++) { int x=get(s[i]); if (!tr[p][x]) tr[p][x]=++idx; p=tr[p][x], cnt[p]++; } } int query(char *s) { int p=0, n=strlen(s+1); for (int i=1; i<=n; i++) { int x=get(s[i]); if (!tr[p][x]) return 0; p=tr[p][x]; } return cnt[p]; }
01 Trie
Trie 是解决异或问题的有力数据结构。可以方便的维护异或对。
我们把数字拆成二进制数,就能组成 {0, 1} 字符集的 Trie 树了。
下面给出一些好题。
[维护异或极值] P10471
暴力:枚举每个 a[i],枚举每个 a[i]。我们考虑加速找 j 的过程,即考虑快速找到最大的 a[j],使得 a[i]^a[j] 取最大。
先考虑拆位,然后把二进制结果从高到低挂到 trie 上。(注意二进制结果位数应该一致。本题需要补成 31 位)
这里要优先考虑高位,因为由于贪心去考虑(具体在下面),高位压过低位。
然后贪心的想:a 当前位是 0,就往 Trie 的右边走(右边是 1),如果无法往右边走,就只能往左边走了;a 当前位是 1,就往 Trie 的左边走(左边是 0),如果无法往左边走,就只能往右边走了。
这样能使异或结果尽可能大。我们把路径上的 01 乘上其权重,加起来就是 a[j] 了。
[维护异或极值] P4551
因为是异或,且都是边权而无点权,有 dis(u, v)=dis(u, root)^dis(v, root)
dis[u] 表示 u 到跟的距离,对于每个 dis[u] 只要找出最大的 dis[v] 即可。同上题。
01Trie 本质
位运算 之 小 trick - cn是大帅哥886 - 博客园
异或 => 异或前缀和 => 5726. 连续子序列 - AcWing题库,看看这题的题解,非常之精巧的一题。
例题
[trie应用] AT_abc403_e
AT_abc403_e [ABC403E] Forbidden Prefix - 洛谷
每次加入 y,沿路 y标记 +1,顺便更新答案。没有经过 x 标记就 +1.
x 的前缀不重复加入,每次加入了 x 就现在末尾打 x标记,下次经过了就不往下走。
顺便把之前经过的所有 y 的标记减去 x所在结点的y标记值,并减去对答案的贡献。
(当然这题也可以维护一颗子树,更容易理解些)
1.字典树是由链组成的,所以沿路减去贡献不用记录 fa,直接再重新重头遍历一遍即可。
2.在字典树中,不想访问 x 往后的结点(也可以理解为不包含某个前缀),直接在此处打标记即可。
3.需要区分是沿路打标记,还是末尾打标记。沿路打标记:统计子树和,末尾打标记:处理一类前缀问题。