Luogu P9595 「Daily OI Round 1」Xor 题解 [ 蓝 ] [ 01 Trie ] [ 二叉树 ] [ 线段树 ]
Xor:有点牛的题。
Sol.1 普通 01 Trie 做法
应该是比较套路的一种做法了。首先考虑静态问题怎么做,先把值域里的所有数丢到 01 Trie 内,然后再将 \(A\) 序列里的数在 01 Trie 上查询,查询到的节点打上一个 Tag。最后 dfs 一遍,合并两个儿子就类似于最大子段和,写个 pushup 函数即可。值得注意的是一颗满的值域为 \(2^i\) 的 01 Trie 一共有 \(2^{i+1}\) 个节点,所以空间并不会炸掉。
再考虑带修时的暴力做法。假如序列里的每个数都异或了 \(x\),那么对于二进制下的 \(x\),假如第 \(i\) 位是 \(1\),则 01 Trie 上第 \(i\) 层的节点左右儿子顺序全都要交换,因为需要保证左儿子代表的数比右儿子小。由此每次暴力遍历一遍 01 Trie,时间复杂度 \(O(n^2)\)。
进一步观察性质,发现每个数可能被异或的值最多只有 \(O(V)\) 个,于是考虑能不能把所有的询问一次全部求出答案。又因为 01 Trie 是一颗满二叉树,很容易能想到和 maze 一样的 trick:在满二叉树上 DP,每个节点上只记录 \(O(size)\) 个信息,容易证明这样做的时间复杂度为 \(O(V\log V)\) 的。
具体而言,每个节点 \(u\) 上记录其子树内异或值,因为子树的深度为 \(dep_u - 1\),所以有用的异或值一共只有 \(O(size)\) 个。然后在 DP 转移的时候,判断最高位是否为 \(1\),如果是的话交换左右儿子 pushup,否则做正常 pushup 即可。
代码里每个节点的 DP 值用了 vector 动态开空间,但实际上内存池就可以直接替代,没必要像我写的一样复杂。
#include <bits/stdc++.h>
#define fi first
#define se second
#define eb(x) emplace_back(x)
#define pb(x) push_back(x)
#define lc(x) (tr[x].ls)
#define rc(x) (tr[x].rs)
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double ldb;
using pi = pair<int, int>;
const int N = 1100005;
int n, q, a[N], ch[N][2], idx, lv[N];
struct Node{
int len, lx, rx, ans;
};
vector<Node> dp[N];
void insert(int x)
{
int p = 0;
for(int i = 18; i >= 0; i--)
{
int c = ((x >> i) & 1);
if(ch[p][c] == 0) ch[p][c] = ++idx;
p = ch[p][c];
lv[p] = i;
}
}
void query(int x)
{
int p = 0;
for(int i = 18; i >= 0; i--)
{
int c = ((x >> i) & 1);
p = ch[p][c];
}
dp[p][0].lx = dp[p][0].rx = dp[p][0].ans = 1;
}
void pushup(Node &p, Node ls, Node rs)
{
p.ans = max(max(ls.ans, rs.ans), ls.rx + rs.lx);
p.lx = ls.lx;
if(ls.lx == ls.len) p.lx = ls.len + rs.lx;
p.rx = rs.rx;
if(rs.rx == rs.len) p.rx = rs.len + ls.rx;
}
void dfs(int p)
{
int sz = dp[p][0].len;
if(lv[p] == 0) return;
dfs(ch[p][0]);
dfs(ch[p][1]);
for(int i = 0; i < sz; i++)
{
if(i >= (sz >> 1))
pushup(dp[p][i], dp[ch[p][1]][i ^ (1 << (lv[p] - 1))], dp[ch[p][0]][i ^ (1 << (lv[p] - 1))]);
else
pushup(dp[p][i], dp[ch[p][0]][i], dp[ch[p][1]][i]);
}
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> q;
for(int i = 0; i < (1 << 19); i++)
insert(i);
lv[0] = 19;
for(int i = 0; i <= idx; i++)
{
int sz = (1 << (lv[i]));
dp[i].resize(sz);
for(int j = 0; j < sz; j++)
dp[i][j].len = sz;
}
for(int i = 1; i <= n; i++)
{
cin >> a[i];
query(a[i]);
}
dfs(0);
int cur = 0;
while(q--)
{
int x;
cin >> x;
cur ^= x;
cout << dp[0][cur].ans << "\n";
}
return 0;
}
Sol.2 可持久化 01 Trie 做法
大体做法与上个做法一致。因为 01 Trie 本质上是一颗权值线段树,所以也是可以持久化的,而本质不同的线段树很少(第 \(i\) 层线段树共有 \(2^{i}\) 个节点,\(2^{dep - i}\) 种不同子树,所以一共只有 \(dep\times2^{dep}\) 种本质不同的线段树),所以考虑对每一个询问开一个根,然后逐层对每个节点 pushup 即可。时间复杂度 \(O(V\log V)\)。
同时这种做法可以进一步拓展至询问的 \(x\) 是一个区间的问题,在官解中已经提及,不再赘述。

浙公网安备 33010602011771号