01 trie
前言
引用一句刘邦的话:“有时 01 Trie
胜于 01 Trie
。”
正文
前景提要
- Trie(串串咕了 qwq)。
- 位运算。
- 贪心。
01 trie
简介
实质上是字符集只有 \(\left \{ 0,1 \right \}\) 的 trie。
由于每一个自然数一定可以进行二进制拆分,所以一个数其实可以看成一个二进制下的 01 串。
所以 01 trie 是可以高效维护序列上的位运算问题的。
另外由于 01 trie 维护的字符串实质上是二进制拆分后的一个数,其长度为 \(O(\log V)\),所以 01 trie 的树高是 \(\log V\) 的,时间复杂度是 \(O(n \log V)\) 的。
代码解析
01 trie 不同于其它 ds,其运作主要是基于贪心实现的,并且要求选手对二进制又一定的熟悉程度。
以 CF923C 为例,对 01 trie 的代码进行解析。
CF923C
- 分析
1.题目要求让 \(c\) 序列字典序最小,那么就是让 \(c\) 越靠前的元素越小。
2.此时题目转化为异或最小值问题,可以考虑使用 01 trie。
3.倘若我们考虑完第 \(i\) 个元素,则第 \(i\) 个元素往后不能继续贡献,因此要在 01 trie 上删除第 \(i\) 个元素。
- 代码解析:
\(\circ\) 变量定义:
const int V = 31; // 表示维护的值域拆成二进制下的最高位数
int tot; // 01 trie 上节点总数
struct Trie {
int sz // 可以用来维护诸如 trie 树上的子树和之类的东西,此处并未用到
, cnt // 维护到当前节点的前缀的出现次数
, ch[2]; // 字符集,01 trie 的字符集就是 0/1
inline int operator [] (const bool x) const { // 个人代码习惯重载
return ch[x];
}
} t[N << 5]; // 保险起见未限制空间时开 5 倍空间
\(\circ\) 插入数字:
inline void insert(int x) {
int p = 0; // 当前节点
for(int i = V - 1 ; ~ i ; -- i) {
bool w = x & (1 << i); // 要插入的数字从高往低的二进制位
if(! t[p][w]) t[p].ch[w] = ++ tot; // 若此前没有节点则新建
p = t[p][w], ++ t[p].cnt; // 迭代节点,统计前缀出现次数
}
}
\(\circ\) 查询:
// 基本上 01 trie 的入门题变化就出现在查询时的贪心考虑
inline int query(int x) {
int p = 0, res = 0; // 当前节点与使得与 x 异或值最小的答案
for(int i = V - 1 ; ~ i ; -- i) {
bool w = x & (1 << i);
if(! t[t[p][w]].cnt) w ^= 1; // 贪心,尽量要让二进制下两位相同异或和才能小,若不同才选择另外一个
res = (res << 1) | w; // 统计答案
p = t[p][w];
-- t[p].cnt; // 查询时顺便删除
}
return res ^ x; // 记得异或 x
}