异或盒子(01-Trie + 全局 +1)

异或盒子

一、题目描述

有一个包含 n 个元素的盒子,每个元素有权值 v_i。需要支持三种操作:

  • op = 0:将 v_x 修改为 p
  • op = 1:将所有元素权值 +1
  • op = 2:查询所有元素的异或和

对应题目:洛谷 P11872 [威海市赛 2024] 异或盒子 1

二、思路分析

暴力做法的问题

  • 如果每次 op = 1 都遍历所有元素加 1,单次 O(n),总复杂度 O(nq),会超时。
  • 需要一种结构,既能快速查询整体异或和,又能高效处理「全局 +1」。

关键观察:全局 +1 在二进制下的表现

对于一个二进制数 x,x + 1 的效果是:

  • 从最低位开始,把连续的 1 全部变成 0
  • 把第一个遇到的 0 变成 1

例如:

x  = 1011
x+1= 1100
x  = 0111
x+1= 1000

这个操作可以看作:从低位到高位,遇到 1 就翻转成 0 并继续进位,遇到 0 就翻转成 1 并停止。

用 Trie 维护

把这 n 个数按从低位到高位的顺序插入一棵 01-Trie。

这样一来,全局 +1 就等价于在 Trie 上从根开始:

  • 交换左右儿子(相当于把当前位的 0 和 1 互换)
  • 然后递归到新的「0 子树」(原来的 1 子树)继续处理进位

这是因为:

  • 原来这一位是 1 的数,加 1 后这一位变成 0,并产生进位
  • 原来这一位是 0 的数,加 1 后这一位变成 1,不再进位

维护每个节点的 bitcnt

为了快速查询所有数的异或和,我们在每个 Trie 节点上维护一个数组:

bitcnt[i] = 该节点子树内,第 i 位为 1 的元素个数

那么整体异或和第 i 位的结果就是:

bitcnt[i] % 2

因为异或和就是所有数按位异或,某一位为 1 当且仅当这一位为 1 的元素个数是奇数。

查询

直接从根节点的 bitcnt 数组读:

for (int i = 0; i < MAXB; i++)
    if (trie[0].bitcnt[i] % 2) res |= (1 << i);

三、操作详解

1. 插入 insert(val)

从根向下,每一位:

  • 更新当前节点的 num 和 bitcnt
  • 如果对应子节点不存在,就新建
  • 走到子节点后继续更新

2. 删除 erase(val)

和插入相反,沿路径把 num 和 bitcnt 减回去。

3. 全局 +1:add(u, pos)

void add(int u, int pos)
{
    if (u == -1 || pos >= MAXB) return;
    swap(trie[u].ch[0], trie[u].ch[1]);   // 交换 0/1 子树
    int L = trie[u].ch[0];                 // 原来的 1 子树(进位方向)
    int R = trie[u].ch[1];                 // 原来的 0 子树(不产生进位)

    add(L, pos + 1);                       // 递归处理进位

    if (L != -1) trie[L].bitcnt[pos] = 0;  // 这一位原来为 1,现在变成 0
    if (R != -1) trie[R].bitcnt[pos] = trie[R].num; // 这一位原来为 0,现在变成 1

    // 重新汇总当前节点的 bitcnt
    for (int k = 0; k < MAXB; k++)
    {
        trie[u].bitcnt[k] = 0;
        if (trie[u].ch[0] != -1) trie[u].bitcnt[k] += trie[trie[u].ch[0]].bitcnt[k];
        if (trie[u].ch[1] != -1) trie[u].bitcnt[k] += trie[trie[u].ch[1]].bitcnt[k];
    }
}

关键点:

  • 交换左右儿子,相当于把「当前位为 0/1」的集合互换
  • 递归处理原来的 1 子树(会产生进位)
  • 原来的 0 子树不产生进位,只需要把当前位的 bitcnt 更新为全部元素
  • 最后重新汇总当前节点的 bitcnt

4. 修改 op = 0

erase(v[x] + base);
insert(p);
v[x] = p - base;

用 base 记录全局加了多少次 1,插入时用 v[x] + base 还原真实值,删除后插入新值 p,并把 v[x] 保存为 p - base。

四、AC 代码

#include<bits/stdc++.h>
using namespace std;

const int MAXB = 20;
const int N = 5e5 + 5;
int v[N];
struct Node
{
    int ch[2];
    int num;
    int bitcnt[MAXB];
    Node()
    {
        ch[0] = ch[1] = -1;
        num = 0;
        memset(bitcnt, 0, sizeof(bitcnt));
    }
};
vector<Node> trie;
int createNode()
{
    trie.emplace_back(Node());
    return (int)trie.size() - 1;
}
void insert(int val)
{
    int u = 0;
    trie[u].num++;
    for (int i = 0; i < MAXB; i++)
        if (val & (1 << i)) trie[u].bitcnt[i]++;

    for (int i = 0; i < MAXB; i++)
    {
        int bit = (val >> i) & 1;
        if (trie[u].ch[bit] == -1)
            trie[u].ch[bit] = createNode();
        u = trie[u].ch[bit];
        trie[u].num++;
        for (int k = 0; k < MAXB; k++)
            if (val & (1 << k)) trie[u].bitcnt[k]++;
    }
}

void erase(int val)
{
    int u = 0;
    trie[u].num--;
    for (int i = 0; i < MAXB; i++)
        if (val & (1 << i)) trie[u].bitcnt[i]--;

    for (int i = 0; i < MAXB; i++)
    {
        int bit = (val >> i) & 1;
        u = trie[u].ch[bit];
        trie[u].num--;
        for (int k = 0; k < MAXB; k++)
            if (val & (1 << k)) trie[u].bitcnt[k]--;
    }
}

void add(int u, int pos)
{
    if (u == -1 || pos >= MAXB) return;
    swap(trie[u].ch[0], trie[u].ch[1]);
    int L = trie[u].ch[0];
    int R = trie[u].ch[1];

    add(L, pos + 1);

    if (L != -1) trie[L].bitcnt[pos] = 0;
    if (R != -1) trie[R].bitcnt[pos] = trie[R].num;

    for (int k = 0; k < MAXB; k++)
    {
        trie[u].bitcnt[k] = 0;
        if (trie[u].ch[0] != -1) trie[u].bitcnt[k] += trie[trie[u].ch[0]].bitcnt[k];
        if (trie[u].ch[1] != -1) trie[u].bitcnt[k] += trie[trie[u].ch[1]].bitcnt[k];
    }
}
int query()
{
    int res = 0;
    for (int i = 0; i < MAXB; i++)
        if (trie[0].bitcnt[i] % 2) res |= (1 << i);
    return res;
}
int main()
{
    int n, q;
    int base = 0;
    cin >> n >> q;
    trie.push_back(Node());

    for (int i = 1; i <= n; i++)
    {
        cin >> v[i];
        insert(v[i]);
    }
    for (int i = 1; i <= q; i++)
    {
        int op;
        cin >> op;
        if (op == 0)
        {
            int x, p;
            cin >> x >> p;
            erase(v[x] + base);
            insert(p);
            v[x] = p - base;
        }
        else if (op == 1)
        {
            add(0, 0);
            base++;
        }
        else if (op == 2) cout << query() << endl;
    }
    return 0;
}

五、易错点

  • Trie 要按 从低位到高位 插入(与普通 01-Trie 相反),因为进位是从低位开始的
  • base 记录全局加 1 的次数,插入/删除时要用 v[x] + base 还原真实值
  • add 递归中要重新汇总 bitcnt,不能只更新当前位
  • 交换左右子树后,原来的 1 子树会变成新的 0 子树(进位方向)

六、总结

要点 内容
核心结构 按低位到高位插入的 01-Trie
关键技巧 全局 +1 = 在 Trie 上交换左右子树 + 递归进位
查询方式 维护每个节点的 bitcnt 数组,按位统计奇偶
复杂度 单次操作 O(MAXB²)
posted @ 2026-09-24 14:59  leaderling  阅读(8)  评论(0)    收藏  举报