Appearance
普通 01-Trie
01-Trie 与 Trie 结构相同,区别在于维护的字符集是
本文模板约定键为非负 int,由调用者传入最高处理位
点击展开代码
cpp
struct Trie01 {
int root = 0, idx = 0;
int son[(N << 5) + 1][2]{};
void init() {
for (int i = 0; i <= idx; i++)
memset(son[i], 0, sizeof son[i]);
root = idx = 0;
}
void insert(int &p, int v, int x) {
if (!p) {
assert(idx < (N << 5));
p = ++idx;
son[p][0] = son[p][1] = 0;
}
if (x < 0) {
return;
}
int now = (v >> x) & 1;
insert(son[p][now], v, x - 1);
}
int query_xor_max(int p, int v, int x) {
assert(p);
if (x < 0) {
return 0;
}
int now = (v >> x) & 1;
if (son[p][now ^ 1])
return query_xor_max(son[p][now ^ 1], v, x - 1) | (1 << x);
return query_xor_max(son[p][now], v, x - 1);
}
};可持久化 01-Trie
一般可持久化 Trie 仅用于 01-Trie,通过差分可维护区间二进制信息。
每次插入会复制一条长度为
模板约定 root[i] 表示插入前 p = root[l - 1]、q = root[r],并且必须保证
点击展开代码
cpp
struct PTrie01 {
int root[N + 1]{}, idx = 0;
int sz[(N << 5) + 1]{}, son[(N << 5) + 1][2]{};
void init(int n) {
assert(0 <= n && n <= N);
for (int i = 0; i <= idx; i++){
memset(son[i], 0, sizeof son[i]);
sz[i] = 0;
}
for (int i = 0; i <= n; i++)
root[i] = 0;
idx = 0;
}
void insert(int p, int &q, int v, int x) {
if (!q) {
assert(idx < (N << 5));
q = ++idx;
son[q][0] = son[q][1] = 0;
sz[q] = 0;
}
sz[q] = sz[p];
sz[q]++;
if (x < 0)
return;
int now = (v >> x) & 1;
son[q][now ^ 1] = son[p][now ^ 1];
insert(son[p][now], son[q][now], v, x - 1);
}
int query_xor_max(int p, int q, int v, int x) {
assert(sz[q] > sz[p]);
if (x < 0) {
return 0;
}
int now = (v >> x) & 1;
if (sz[son[q][now ^ 1]] - sz[son[p][now ^ 1]] > 0)
return query_xor_max(son[p][now ^ 1], son[q][now ^ 1], v, x - 1) |
(1 << x);
return query_xor_max(son[p][now], son[q][now], v, x - 1);
}
};压缩 01-Trie
Patricia Trie
容易发现,01-Trie 在插入时,只有当同时拥有左右儿子时,才会分叉。
不分叉时,单链的递归路径是唯一的,那么就可以尝试把它压缩起来。
引理:对于一棵除了叶子,其他节点儿子数都是
的树而言,非叶子数 叶子数 。
设当前不同元素的数量为
在时间复杂度 N 必须按整个操作序列中可能创建的节点总数预留,不能只按某一时刻的集合大小估算。
dep 表示当前子树可以继续分叉的位置上界(不含)。对非负 int 的第 insert(root, 31, v),删除时对应调用 remove(root, 31, v)。
且因为保证了非叶子的左右儿子一定是存在的,所以查询比 01-Trie 更加方便。
点击展开代码
cpp
struct node
{
int son[2], dep, val;
int cnt;
};
struct PatriciaTrie
{
node tr[(N << 1) + 1]{};
int root = 0, idx = 0;
void init()
{
for (int i = 0; i <= idx; i++)
tr[i] = {};
root = idx = 0;
}
void insert(int &p, int dep, int v)
{
assert(v >= 0 && 0 <= dep && dep <= 31);
if (!p)
{
assert(idx + 1 <= (N << 1));
p = ++idx;
tr[p] = {{0, 0}, -1, v, 1};
return;
}
int diff = v ^ tr[p].val;
int mask1 = (1ll << dep) - 1;
int mask2 = (tr[p].dep == -1) ? ~0 : ~((1ll << (tr[p].dep + 1)) - 1);
diff &= (mask1 & mask2);
if (diff)
{
assert(idx + 2 <= (N << 1));
int high = __lg(diff), old = p;
int now = ++idx, leaf = ++idx;
tr[now] = {{0, 0}, high, tr[old].val, 0};
tr[leaf] = {{0, 0}, -1, v, 1};
tr[now].son[(tr[old].val >> high) & 1] = old;
tr[now].son[(v >> high) & 1] = leaf;
p = now;
return;
}
if (tr[p].dep == -1)
{
tr[p].cnt++;
return;
}
insert(tr[p].son[(v >> tr[p].dep) & 1], tr[p].dep, v);
}
bool remove(int &p, int dep, int v)
{
assert(v >= 0 && 0 <= dep && dep <= 31);
if (!p)
return 0;
int diff = v ^ tr[p].val;
int mask1 = (1LL << dep) - 1;
int mask2 = (tr[p].dep == -1) ? ~0 : ~((1LL << (tr[p].dep + 1)) - 1);
if (diff & mask1 & mask2)
return 0;
if (tr[p].dep == -1)
{
if (tr[p].val != v)
return 0;
assert(tr[p].cnt > 0);
if (--tr[p].cnt == 0)
p = 0;
return 1;
}
int now = (v >> tr[p].dep) & 1;
if (!remove(tr[p].son[now], tr[p].dep, v))
return 0;
if (!tr[p].son[now])
{
p = tr[p].son[now ^ 1];
}
else if (tr[p].val == v)
{
tr[p].val = tr[tr[p].son[0]].val;
}
return 1;
}
int query_max(int v)
{
assert(v >= 0 && root);
int now = root;
while (tr[now].dep != -1)
{
int cur = (v >> tr[now].dep) & 1;
now = tr[now].son[cur ^ 1];
}
return v ^ tr[now].val;
}
};Patricia Trie 同样可以持久化,但是时空复杂度与可持久化 01-Trie 一致,所以不作展开。
压位 01-Trie
压位 01-Trie 本质是一棵 unsigned long long 的位数)。
每个节点用一个 unsigned long long 压缩儿子节点状态,如果
通过位运算计算访问相应的节点。
因为压位 01-Trie 只存储「是否存在儿子」的信息,所以不显示建树,直接将每一层的节点拍平成数组。
默认维护值域 std::optional<int>,std::nullopt 表示空集或不存在满足条件的元素。空间复杂度为
代码使用 std::optional,需要 C++17 或更高标准及 <optional>。insert/erase 返回集合是否实际发生变化,越界、重复插入和删除不存在元素均返回 false。
contains 的时间复杂度为
插入/删除
遍历每一层,更新对应节点的儿子节点状态即可。
前驱/后继
先自下而上,向前/后找到第一个可转向的层(也就是子树存在叶子的节点),再向下找到那个点。
最大值/最小值
其实可以直接转换成判断值域的边界是否存在后查前驱后继。
自上而下,找到最高/最低位
点击展开代码
cpp
using u64 = unsigned long long;
struct FastTrie
{
int V = -1;
vector<vector<u64>> tr;
vector<int> nbits;
void init(int V)
{
assert(0 <= V && V < INT_MAX);
this->V = V;
tr.clear();
nbits.clear();
int idx = 0;
int now = V + 1;
while (1)
{
idx++;
int cur = (int)((1LL * now + 63) >> 6);
if (cur == 1)
break;
now = cur;
}
tr.reserve(idx);
nbits.reserve(idx);
now = V + 1;
while (1)
{
nbits.emplace_back(now);
int cur = (int)((1LL * now + 63) >> 6);
tr.emplace_back(cur, 0ULL);
if (cur == 1)
break;
now = cur;
}
}
int lowbit_pos(u64 x)
{
return __builtin_ctzll(x);
}
int highbit_pos(u64 x)
{
return 63 - __builtin_clzll(x);
}
u64 prefix_mask(int b)
{
return b == 63 ? ~0ULL : ((1ULL << (b + 1)) - 1);
}
bool empty()
{
return tr.empty() || tr.back()[0] == 0;
}
bool in_range(int x)
{
return !tr.empty() && 0 <= x && x <= V;
}
int next_in_dep(int dep, int pos)
{
if (pos >= nbits[dep])
return -1;
int w = pos >> 6;
int b = pos & 63;
u64 cur = tr[dep][w] & (~0ULL << b);
if (cur)
{
int res = (w << 6) + lowbit_pos(cur);
return res < nbits[dep] ? res : -1;
}
if (dep + 1 >= (int)tr.size())
return -1;
int nw = next_in_dep(dep + 1, w + 1);
if (nw == -1)
return -1;
int res = (nw << 6) + lowbit_pos(tr[dep][nw]);
return res < nbits[dep] ? res : -1;
}
int prev_in_dep(int dep, int pos)
{
if (pos < 0)
return -1;
if (pos >= nbits[dep])
pos = nbits[dep] - 1;
int w = pos >> 6;
int b = pos & 63;
u64 cur = tr[dep][w] & prefix_mask(b);
if (cur)
{
int res = (w << 6) + highbit_pos(cur);
return res < nbits[dep] ? res : -1;
}
if (dep + 1 >= (int)tr.size())
return -1;
int pw = prev_in_dep(dep + 1, w - 1);
if (pw == -1)
return -1;
int res = (pw << 6) + highbit_pos(tr[dep][pw]);
return res < nbits[dep] ? res : -1;
}
bool contains(int x)
{
if (!in_range(x))
return false;
int w = x >> 6;
int b = x & 63;
return (tr[0][w] >> b) & 1ULL;
}
bool insert(int x)
{
if (!in_range(x))
return false;
int w0 = x >> 6;
int b0 = x & 63;
u64 m0 = 1ULL << b0;
if (tr[0][w0] & m0)
return false;
int id = x;
for (int dep = 0; dep < (int)tr.size(); dep++)
{
int w = id >> 6;
int b = id & 63;
u64 m = 1ULL << b;
u64 old = tr[dep][w];
tr[dep][w] |= m;
if (old)
break;
id = w;
}
return true;
}
bool erase(int x)
{
if (!in_range(x))
return false;
int w0 = x >> 6;
int b0 = x & 63;
u64 m0 = 1ULL << b0;
if (!(tr[0][w0] & m0))
return false;
int id = x;
for (int dep = 0; dep < (int)tr.size(); dep++)
{
int w = id >> 6;
int b = id & 63;
u64 m = 1ULL << b;
tr[dep][w] &= ~m;
if (tr[dep][w])
break;
id = w;
}
return true;
}
std::optional<int> get_min()
{
if (empty())
return std::nullopt;
int top = tr.size() - 1;
int id = lowbit_pos(tr[top][0]);
for (int dep = top - 1; dep >= 0; dep--)
{
id = (id << 6) + lowbit_pos(tr[dep][id]);
}
return id;
}
std::optional<int> get_max()
{
if (empty())
return std::nullopt;
int top = tr.size() - 1;
int id = highbit_pos(tr[top][0]);
for (int dep = top - 1; dep >= 0; dep--)
{
id = (id << 6) + highbit_pos(tr[dep][id]);
}
return id;
}
std::optional<int> get_prev(int x)
{
if (x <= 0 || empty())
return std::nullopt;
int p = min(x - 1, V);
int res = prev_in_dep(0, p);
if (res == -1)
return std::nullopt;
return res;
}
std::optional<int> get_next(int x)
{
if (x >= V || empty())
return std::nullopt;
int p = max(0, x + 1);
int res = next_in_dep(0, p);
if (res == -1)
return std::nullopt;
return res;
}
};动态开点 压位 01-Trie
如果值域达到
一个办法是直接恢复到显式地把树建出来(因为 01-Trie 本来就是动态开点的,所以其实就是恢复到了原本的写法),但是这样要存儿子节点的编号。
如果直接存,空间复杂度为 map 维护儿子,时空复杂度均多乘一个
可以用于集合大小不大,但是询问次数爆炸、值域爆炸,并且强制在线的情况。
本节只记录思路,暂未给出可复制的实现。实现时仍需统一约定值域为
点击展开代码
cpp
// 规划中:暂无可复制实现。