本文略过了 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 简记的更多相关文章

  1. 可持久化0-1 Trie 简介

    Trie树是字符串问题中应用极为广泛的一种数据结构,可以拓展出AC自动机.后缀字典树等实用数据结构. 然而在此我们考虑0-1 Trie的应用,即在序列最大异或问题中的应用. 这里的异或是指按位异或.按 ...

  2. hdu 4825 Xor Sum (01 Trie)

    链接:http://acm.hdu.edu.cn/showproblem.php?pid=4825 题面: Xor Sum Time Limit: 2000/1000 MS (Java/Others) ...

  3. 51nod 1295 XOR key 可持久化01字典树

    题意 给出一个长度为\(n\)的正整数数组\(a\),再给出\(q\)个询问,每次询问给出3个数,\(L,R,X(L<=R)\).求\(a[L]\)至\(a[R]\)这\(R-L+1\)个数中, ...

  4. HDU 6191 2017ACM/ICPC广西邀请赛 J Query on A Tree 可持久化01字典树+dfs序

    题意 给一颗\(n\)个节点的带点权的树,以\(1\)为根节点,\(q\)次询问,每次询问给出2个数\(u\),\(x\),求\(u\)的子树中的点上的值与\(x\)异或的值最大为多少 分析 先dfs ...

  5. [一本通学习笔记] 字典树与 0-1 Trie

    字典树中根到每个结点对应原串集合的一个前缀,这个前缀由路径上所有转移边对应的字母构成.我们可以对每个结点维护一些需要的信息,这样即可以去做很多事情. #10049. 「一本通 2.3 例 1」Phon ...

  6. P4735 最大异或和 01 Trie

    题目描述 给定一个非负整数序列 \(\{a\}\),初始长度为\(n\). 有 \(m\) 个操作,有以下两种操作类型: \(A\ x\):添加操作,表示在序列末尾添加一个数 \(x\),序列的长度 ...

  7. 「模板」 01 Trie实现平衡树功能

    不想多说什么了.费空间,也不算太快,唯一的好处就是好写吧. #include <cstdio> #include <cstring> const int MAXN=100010 ...

  8. CSU 1216异或最大值 (0-1 trie树)

    Description 给定一些数,求这些数中两个数的异或值最大的那个值 Input 多组数据.第一行为数字个数n,1 <= n <= 10 ^ 5.接下来n行每行一个32位有符号非负整数 ...

  9. 三元组[01 Trie计数]

    也许更好的阅读体验 \(\mathcal{Description}\) \(\mathcal{Solution}\) 有两种方法都可以拿到满分 \(Solution\ 1\) 考虑枚举\(y\) 建两 ...

  10. poj 3764 The xor-longest Path (01 Trie)

    链接:http://poj.org/problem?id=3764 题面: The xor-longest Path Time Limit: 2000MS   Memory Limit: 65536K ...

随机推荐

  1. GIS矢量数据获取:全球行政区划、路网、POI点、建筑物范围、信号基站等

      本文对目前主要的行政区边界与道路路网.建筑轮廓.POI.手机基站等数据产品的获取网站加以整理与介绍. 目录 5 行政区边界与建筑轮廓.POI.基站数据 5.1 行政区边界数据 5.1.1 DIVA ...

  2. linux mint安装Scala

    Scala由java编写,需要前期安装jdk 面向函数式编程 1.下载 Scala 二进制包2.11.8 http://www.scala-lang.org/downloads 解压到/usr/loc ...

  3. Atcoder ABC387F Count Arrays 题解 [ 绿 ] [ 基环树 ] [ 树形 dp ] [ 前缀和优化 ]

    Count Arrays:一眼秒的计数题. 思路 显然,把小于等于的条件化为大的向小的连单向边,每个数的入度都是 \(1\),就会形成一个基环树森林. 那么考虑这个环上能填什么数.因为所有数都小于等于 ...

  4. Atcoder ABC329E Stamp 题解 [ 绿 ] [ 线性 dp ]

    Stamp:难点主要在 dp 转移的细节与分讨上,但通过改变状态设计可以大大简化分讨细节的题. 观察 首先要有一个观察:只要某一个前缀能被覆盖出来,那么无论它后面多出来多少,后面的字符串都可以帮他重新 ...

  5. 动手学深度学习-python基础知识介绍(数据处理基础流程)part2

    数据预处理 import os os.makedirs(os.path.join('..','data'),exist_ok=True) data_file=os.path.join('..','da ...

  6. vue element UI el-table表格添加行点击事件

    <el-table @row-click="openDetails"></el-table> //对应的 methods 中//点击行事件methods: ...

  7. Spark 广播变量(broadcast)更新方法

    Spark 广播变量(broadcast)更新方法更新方法spark 广播变量可以通过unpersist方法删除,然后重新广播 val map = sc.textFile("/test.tx ...

  8. sql server 使用sql语句导出二进制文件到本地磁盘

    sp_configure 'show advanced options', 1;GORECONFIGURE;GOsp_configure 'Ole Automation Procedures', 1; ...

  9. 【编程思维】临近实施 WPF 下拉框闪烁问题!!

    私以为架构是业务开发的发展历史,顺应大方向而生,再为贴切时刻的用户需求,持续微改动. 我本以为了解这个软件的架构没甚意思,加快的开发速度不能过渡到下一个别的软件去: 却不知以小窥大,关键还是计算机思维 ...

  10. python 两个函数间如何调用

    def a(): pass def b(): pass s=a() b(s) 或者 b(a())