可持久化 01-trie 简记
本文略过了 trie 和 可持久化的介绍,如果没学过请先自学。
在求给定一个值 \(k\) 与区间中某些值的异或最大值时,可以考虑使用在线的数据结构可持久化 01-trie 来维护。
01-trie
01-trie 本身是用以求异或最大值的数据结构。
考虑板子题:给定 \(n\) 个数,\(m\) 次询问 \(k\) 与某个数异或的最大值。
我们肯定不能直接拿 \(k\) 与每个数异或,考虑把 \(k\) 拆成二进制,对于每一位单独做。
从高位到低位贪心。如果 \(k\) 的这一位是 \(1\) 那么我们就要尽可能的让选到的 \(a_i\) 这一位为 \(0\);反之要让 \(a_i\) 这一位为 \(1\)。这样我们取到的 \(a_i\) 一定是使 \(k\oplus a_i\) 最大的值。
这样我们就需要一种可以保留所有二进制串信息且占用空间尽可能小的数据结构,而 trie 恰好符合这一点。
01-trie 就是字符集为 \(\{0, 1\}\) 的 trie。
我们把每个数转成二进制从高位开始加入到 01-trie 中,每次根据 \(k\) 二进制位是 0/1 来贪心地更新答案即可。
复杂度 \(O(n\log v+m\log v)\),\(v\) 是值域。
可持久化思想
如果从一个数变成一个区间与 \(k\) 的异或最大值,我们就要使用可持久化思想来升级 01-trie。
考虑升级版板子题:给定 \(n\) 个数,\(m\) 次询问 \(k\) 与 \(a_l\sim a_r\) 异或后缀异或的最大值,或者插入一个数。
我们也不能对于每个异或后缀开一棵 01-trie 来统计,这样时间空间双双爆炸。考虑差分,转化成前缀信息来做。
记 \(s_i=\oplus_{j=1}^{i}\),询问时找 \(k\oplus s_n\oplus s_{i-1}\) 最大值即是 \(k\) 与 \(a_i\) 后缀异或最大值。
用可持久化思想,每次插入一个数就在前一个版本基础上更新,把新的 \(s_n\) 插入到 01-trie 里面。
根据上面的转化,查询时就可以直接查 \(k\oplus s_n\) 与 \(rt_l\sim rt_r\) 这些版本的 01-trie 的异或最大值。
与没有持久化的 01-trie 类似,考虑对于两个版本的 01-trie 该怎么判断这一位能不能取 \(1\)。
对 01-trie 上每条边 0/1 额外维护一个 sz[]
,表示这条边 0/1 在历史版本中一共出现的次数,加入新节点时就在上一个版本对应边基础上 +1
。查询时就可以作差求出需要的 0/1 边是否存在。
这样我们就用同样的单 \(\log\) 时间复杂度解决了这个问题,唯一区别是可持久化占用的空间从线性 \(O(n)\) 变成了单 \(\log\) \(O(n\log n)\)。
代码实现
原题跳转:Luogu P4735 最大异或和
注意数组范围要开大 \(64\) 倍防止越界。
#include<bits/stdc++.h>
using namespace std;
const int maxn = 6e5 + 10;
int n, m, s[maxn << 1];
int tot, ch[maxn << 5][2], sz[maxn << 5], rt[maxn << 1];
void add(int u, int v, int t, int x) {
if(t < 0) return;
int i = (x >> t) & 1;
ch[u][i] = ++tot, ch[u][i ^ 1] = ch[v][i ^ 1];
sz[ch[u][i]] = sz[ch[v][i]] + 1;
add(ch[u][i], ch[v][i], t - 1, x);
}
int ask(int u, int v, int t, int x) {
if(t < 0) return 0;
int i = (x >> t) & 1;
if(sz[ch[u][i ^ 1]] - sz[ch[v][i ^ 1]]) {
return (1 << t) + ask(ch[u][i ^ 1], ch[v][i ^ 1], t - 1, x);
}
else return ask(ch[u][i], ch[v][i], t - 1, x);
}
int main() {
ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n >> m;
rt[0] = ++tot, add(rt[0], 0, 25, 0);
for(int i = 1; i <= n; i++) {
int x; cin >> x;
s[i] = s[i - 1] ^ x;
rt[i] = ++tot, add(rt[i], rt[i - 1], 25, s[i]);
}
for(int i = 1; i <= m; i++) {
char op; cin >> op;
if(op == 'A') {
int x; cin >> x; ++n;
s[n] = s[n - 1] ^ x, rt[n] = ++tot; add(rt[n], rt[n - 1], 25, s[n]);
}
if(op == 'Q') {
int l, r, x; cin >> l >> r >> x; l--, r--;
if(l == 0) cout << ask(rt[r], 0, 25, x ^ s[n]) << endl;
else cout << ask(rt[r], rt[l - 1], 25, x ^ s[n]) << endl;
}
}
return 0;
}
例题
Luogu P5795 异或运算
给定 \(x\) 和 \(y\) 数组,询问给定两个范围 \(u,d\)、\(l,r\) 和 \(k\),求 \(x_u\sim x_d\) 分别异或 \(y_l\sim y_r\) 的第 \(k\) 大值。其中 \(x\) 长度不超过 \(10^3\),\(y\) 长度不超过 \(3\times 10^5\),询问不超过 \(5\times 10^2\)。
注意这个题数据范围非常抽象。两维分开维护很困难,但是一维可以直接可持久化 01-trie。那么考虑对 \(x\) 暴力,对 \(y\) 用可持久化 01-trie 维护。每次询问,暴力统计 \(x\) 数组中的贡献。
具体地,把 \(x_i\) 的每一位单独拿出来,由于是第 \(k\) 大,统计这一位异或起来为 \(1\) 的个数,如果大于等于 \(k\),就都走二进制位不同的那一位并且统计这一位的贡献;小于 \(k\) 就走二进制位相同的那一项。所有贡献加起来即可。
代码实现
#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e3 + 10, maxm = 3e5 + 10;
int n, m, q, x[maxn], y[maxm];
int tot, ch[maxm << 6][2], sz[maxm << 6], rt[maxm], posl[maxn], posr[maxn];
void add(int u, int v, int t, int k) {
if(t < 0) return;
int i = (k >> t) & 1;
ch[u][i] = ++tot, ch[u][i ^ 1] = ch[v][i ^ 1];
sz[ch[u][i]] = sz[ch[v][i]] + 1;
return add(ch[u][i], ch[v][i], t - 1, k), void(0);
}
int ask(int xl, int xr, int yl, int yr, int k) {
for(int j = xl; j <= xr; j++) posl[j] = rt[yl - 1], posr[j] = rt[yr];
int res = 0;
for(int t = 31; t >= 0; t--) {
int cnt = 0;
for(int j = xl; j <= xr; j++) {
int i = (x[j] >> t) & 1;
cnt += sz[ch[posr[j]][i ^ 1]] - sz[ch[posl[j]][i ^ 1]];
}
if(cnt >= k) {
res |= (1 << t);
for(int j = xl; j <= xr; j++) {
int i = (x[j] >> t) & 1;
posl[j] = ch[posl[j]][i ^ 1];
posr[j] = ch[posr[j]][i ^ 1];
}
}
else {
k -= cnt;
for(int j = xl; j <= xr; j++) {
int i = (x[j] >> t) & 1;
posl[j] = ch[posl[j]][i];
posr[j] = ch[posr[j]][i];
}
}
}
return res;
}
int main() {
ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> x[i];
for(int i = 1; i <= m; i++) cin >> y[i], rt[i] = ++tot, add(rt[i], rt[i - 1], 31, y[i]);
cin >> q;
for(int i = 1; i <= q; i++) {
int u, d, l, r, k; cin >> u >> d >> l >> r >> k;
cout << ask(u, d, l, r, k) << endl;
}
return 0;
}
lxl 上课讲到的题,如果学过可持久化 01-trie 就是宝宝题
Loj #6144. 「2017 山东三轮集训 Day6」C
给定 \(n\) 个数的数组,\(m\) 次询问,每次全局按位异或/按位与/按位或上 \(x\),或者查询 \([l,r]\) 第 \(k\) 大。\(n,m\le5\times 10^4\)。
操作难以直接维护,因为手玩会发现几个按位运算放在一起并不具备交换律,这意味着直接维护需要在线处理标记。但是单独按位异或操作是好维护的,因为异或上某一位的要么不变要么 \(0/1\) 互换,同时按位异或也是可以离线下来等到查询时一起处理的。
考虑记翻转标记 rev
,二进制位为 \(1\) 表示进行 \(0/1\) 翻转,为 \(0\) 时操作不会造成影响。每次要全局异或上 \(x\) 时就可以异或在 rev
上面,查询时就根据标记来判断往那边走。
接下来考虑按位与和按位或操作的维护。发现其都具有合并的性质:具体地,无论 \(0/1\) 按位与上 \(0\)都会得到 \(0\),无论 \(0/1\) 按位或上 \(1\) 都会得到 \(1\),相当于 01-trie 上把两条边缩成一条边;并且合并是不可逆的,因为无论什么操作都不能再将这一位分开。同时由于其会影响 rev
标记,我们需要同步更新:按位与 \(0\) 的 rev
这一位也变成 \(0\);按位或 \(1\) 的 rev
这一位也变成 \(1\)。至于按位与 \(1\) 和按位或 \(0\) 不会造成任何影响。
对于具有合并性质的操作,可能改变 \(O(n\log n)\) 的信息,所以可以考虑直接暴力重构。一次合并会把所有数的这一位变得相同,我们用 vis
记录下二进制位是否经历过合并,对同一位的重复合并不会带来更多便利,没有意义。这样重构至多只会有 \(O(\log v)\) 次,这一部分总复杂度 \(O(n\log^2v)\)。
对于经历过合并的二进制位,我们单独处理它的值。记 tag
表示二进制位被合并之后的值。一个很好的性质是 tag
的值就是每次操作的叠加:按位与 \(0\) 之后它们都变为 \(0\);按位或 \(1\) 之后它们都变为 \(1\);按位异或 \(1\) 之后它们都 \(0/1\) 翻转;其他操作后它们都不发生改变。所以我们只需要每次操作之后更新 tag
就好了。
对于查询,与上道题目类似。查询第 \(k\) 小就找异或上 rev
使这一位为 \(0\) 的数的个数,比 \(k\) 小就统计贡献,并减去个数接着找;对于经历过合并的直接统计贡献即可。
注意加数的时候要特判掉合并过的二进制位
代码实现
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int maxn = 5e4 + 10;
int n, m, a[maxn];
int tot, ch[maxn << 6][2], sz[maxn << 6], rt[maxn];
int tag, rev, vis;
void add(int u, int v, int t, int k) {
if(t < 0) return;
int i = (k >> t) & 1;
if((vis >> t) & 1) i = 0;//合并过的不考虑值
ch[u][i] = ++tot, ch[u][i ^ 1] = ch[v][i ^ 1];
sz[ch[u][i]] = sz[ch[v][i]] + 1;
return add(ch[u][i], ch[v][i], t - 1, k), void(0);
}
void rebuild() {
memset(ch, 0, sizeof ch);
memset(sz, 0, sizeof sz);
memset(rt, 0, sizeof rt);
tot = 0;
for(int i = 1; i <= n; i++) rt[i] = ++tot, add(rt[i], rt[i - 1], 31, a[i] ^ rev);
return rev = 0, void(0);
}
int ask(int u, int v, int t, int k) {
if(t < 0) return 0;
if((vis >> t) & 1) {
return (tag & (1ll << t)) | ask(ch[u][0], ch[v][0], t - 1, k);
}
int i = (rev >> t) & 1, cnt = sz[ch[u][i]] - sz[ch[v][i]];
if(k > cnt) return (1ll << t) | ask(ch[u][i ^ 1], ch[v][i ^ 1], t - 1, k - cnt);
else return ask(ch[u][i], ch[v][i], t - 1, k);
}
signed main() {
ios :: sync_with_stdio(false); cin.tie(0); cout.tie(0);
cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> a[i], rt[i] = ++tot, add(rt[i], rt[i - 1], 31, a[i]);
for(int i = 1; i <= m; i++) {
string op; int x, l, r, k; bool flag = false;
cin >> op;
if(op == "Xor") {
cin >> x;
tag ^= x, rev ^= x;
}
if(op == "And") {
cin >> x;
tag &= x;
for(int t = 31; t >= 0; t--) {
if(!((x >> t) & 1)) {
if(!((vis >> t) & 1)) vis |= (1ll << t), flag = true;
rev -= rev & (1ll << t);// &0:rev第t位变成0
}
}
}
if(op == "Or") {
cin >> x;
tag ^= x;
for(int t = 31; t >= 0; t--) {
if((x >> t) & 1) {
if(!((vis >> t) & 1)) vis |= (1ll << t), flag = true;
rev |= (1ll << t);// |1:rev第t位变成1
}
}
}
if(op == "Ask") {
cin >> l >> r >> k;
cout << ask(rt[r], rt[l - 1], 31, k) << endl;
}
if(flag) rebuild();
}
return 0;
}
但是只有40pts,还没调出来。
可持久化 01-trie 简记的更多相关文章
- 可持久化0-1 Trie 简介
Trie树是字符串问题中应用极为广泛的一种数据结构,可以拓展出AC自动机.后缀字典树等实用数据结构. 然而在此我们考虑0-1 Trie的应用,即在序列最大异或问题中的应用. 这里的异或是指按位异或.按 ...
- hdu 4825 Xor Sum (01 Trie)
链接:http://acm.hdu.edu.cn/showproblem.php?pid=4825 题面: Xor Sum Time Limit: 2000/1000 MS (Java/Others) ...
- 51nod 1295 XOR key 可持久化01字典树
题意 给出一个长度为\(n\)的正整数数组\(a\),再给出\(q\)个询问,每次询问给出3个数,\(L,R,X(L<=R)\).求\(a[L]\)至\(a[R]\)这\(R-L+1\)个数中, ...
- HDU 6191 2017ACM/ICPC广西邀请赛 J Query on A Tree 可持久化01字典树+dfs序
题意 给一颗\(n\)个节点的带点权的树,以\(1\)为根节点,\(q\)次询问,每次询问给出2个数\(u\),\(x\),求\(u\)的子树中的点上的值与\(x\)异或的值最大为多少 分析 先dfs ...
- [一本通学习笔记] 字典树与 0-1 Trie
字典树中根到每个结点对应原串集合的一个前缀,这个前缀由路径上所有转移边对应的字母构成.我们可以对每个结点维护一些需要的信息,这样即可以去做很多事情. #10049. 「一本通 2.3 例 1」Phon ...
- P4735 最大异或和 01 Trie
题目描述 给定一个非负整数序列 \(\{a\}\),初始长度为\(n\). 有 \(m\) 个操作,有以下两种操作类型: \(A\ x\):添加操作,表示在序列末尾添加一个数 \(x\),序列的长度 ...
- 「模板」 01 Trie实现平衡树功能
不想多说什么了.费空间,也不算太快,唯一的好处就是好写吧. #include <cstdio> #include <cstring> const int MAXN=100010 ...
- CSU 1216异或最大值 (0-1 trie树)
Description 给定一些数,求这些数中两个数的异或值最大的那个值 Input 多组数据.第一行为数字个数n,1 <= n <= 10 ^ 5.接下来n行每行一个32位有符号非负整数 ...
- 三元组[01 Trie计数]
也许更好的阅读体验 \(\mathcal{Description}\) \(\mathcal{Solution}\) 有两种方法都可以拿到满分 \(Solution\ 1\) 考虑枚举\(y\) 建两 ...
- poj 3764 The xor-longest Path (01 Trie)
链接:http://poj.org/problem?id=3764 题面: The xor-longest Path Time Limit: 2000MS Memory Limit: 65536K ...
随机推荐
- Amazon Dynamo系统架构
Amazon Dynamo系统架构 目录 Amazon Dynamo系统架构 0x00 摘要 0x01 Amazon Dynamo 1.1 概况 1.2 主要问题及解决方案 1.3 数据均衡分布 1. ...
- 通讯录管理系统(C++基础知识实现)
通讯录管理系统 描述:本人C++小白一枚,正在学习C++基础知识,给大家分享一款使用C++基础知识实现的通讯录管理系统,一起努力进步,大佬轻点喷. 1. 知识点 (1) 预处理器指令 (#includ ...
- AI-接入
前言 前面已经申请了模型,并且通过测试已经可以访问使用了,本篇的接入还是使用Ollama,前面我们已经可以在命令行终端能够进行交互了,现在将AI接入到代码中: 准备 作为一名Neter这里使用的是.n ...
- 2024电子取证“獬豸杯”WP
简介: 竞赛为个人赛,工具自备,只发证书(还没用,公告这么写的哈)竞赛选手们将对模拟的案件进行电子数据调查取证,全面检验参赛选手电子数据取证的综合素质和能力. 检材链接: https://pan.ba ...
- Java 实现 Excel(XLS/ XLSX)和 HTML 格式之间的转换
Excel 是一种电子表格格式,广泛用于数据处理和分析,而HTM则是一种用于创建网页的标记语言.虽然两者在用途上存在差异,但有时我们需要将数据从一种格式转换为另一种格式,以便更好地利用和展示数据.本文 ...
- 用 Emacs 写代码有哪些值得推荐的插件
以下是一些用于 Emacs 写代码的值得推荐的插件: Ido-mode:交互式操作模式,它用列出当前目录所有文件的列表来取代常规的打开文件提示符,能让操作更可视化,快速遍历文件. Smex:可替代普通 ...
- Linux - 搭建一套Apache大数据集群
一.服务器操作系统 主机名 操作系统 node01 Centos 7.9 node02 Centos 7.9 node03 Centot 7.9 二.大数据服务版本 服务 版本 下载 JDK jdk- ...
- 【译】Visual Studio 中新的强大生产力特性
有时候,生活中的小事才是最重要的.在最新版本的 Visual Studio 中,我们增加了一些功能和调整,目的是让您脸上带着微笑,让您更有效率.这里是其中的一些列表,如果您想要完整的列表,请查看发行说 ...
- 使用 HBuilderX 轻松解决 CSS 代码在一行的问题
前言 最近在做博客园的界面美化,用的是园内大佬的开源项目,配置超级简单,只需要复制粘贴代码就好啦. 但在粘贴 CSS 代码时遇到一个问题,那就是所有代码都挤在了一行,没有一点排板的样子(如下图),对我 ...
- JMeter 性能优化
Jmeter 性能优化:(3优化 + 1补充) 1.在 jmx 文件中 Disable 所有的结果输出,如: View Results Tree / Graph Results / Aggrega ...