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\) 是一个区间的问题,在官解中已经提及,不再赘述。

posted @ 2025-08-28 21:28  KS_Fszha  阅读(7)  评论(0)    收藏  举报