省流:gemini 3.1 Pro 对这个经典难题提出了一个优于暴力的做法。

区间加区间异或和怎么做?

具体的,你需要维护一个整数数组,支持两种操作:

  1. 将一个连续区间内所有元素加上一个整数
  2. 查询一个连续区间内的元素的异或和

这个题有任何比较好的做法吗?


这是一个在算法竞赛(如 Codeforces、Luogu)中非常经典的高级数据结构难题

区间加(Addition)和区间异或和(XOR Sum)在本质上是“不兼容”的。因为加法会产生进位(Carry),一个加法操作可能会同时改变多个连续的高位,这导致传统的线段树(Segment Tree)无法在 $O(\log n)$ 的时间内合并和下传 Lazy Tag。

目前解决这个问题最通用且高效的经典做法是:分块(Square Root Decomposition / Block Decomposition),结合按位拆分与二分查找

以下是详细的做法和原理解析:

核心思想:按位拆分 + 余数定理

我们要查询区间异或和,实际上等价于:对于每一位 $k$($k \in [0, 30]$),统计区间内该位为 $1$ 的数字个数。如果个数为奇数,那么异或和的第 $k$ 位就是 $1$。

假设一个数字原来是 $x$,被加上了 $v$。那么 $x+v$ 的第 $k$ 位是否为 $1$,完全取决于它们对 $2^{k+1}$ 的余数。 具体规律如下: 设 $M = 2^{k+1}$, $H = 2^k$。 那么 $(x + v)$ 的第 $k$ 位为 $1$,当且仅当:

$$ (x \bmod M + v \bmod M) \bmod M \ge H $$

如果我们将 $y = x \bmod M$ 看作原数组的属性, $V = v \bmod M$ 看作懒标记(Lazy Tag),因为 $y \in [0, M-1]$ 且 $V \in [0, M-1]$,上述条件其实等价于 $y$ 落在了两个连续的区间内:

  1. $y \in [H - V, M - 1 - V]$
  2. $y \in [M + H - V, M - 1]$

这意味着,如果我们把每一块内的元素按照对 $M$ 取模的值进行排序,当整块被加上 $v$ 时,我们只需要用二分查找(lower_bound / upper_bound)在这两个区间内查一下元素个数,就能在 $O(\log(\text{块大小}))$ 的时间内算出整块的异或和!


具体实现步骤

我们把数组分为大小为 $B \approx \sqrt{N \log N}$ 的块。对于每一个块,我们维护以下信息:

  1. lazy:该块积累的区间加数值。
  2. xor_sum:该块当前的整体异或和(可以直接用于查询,做到 $O(1)$ 查整块)。
  3. 30 个排序好的数组:对应第 $k=0 \dots 29$ 位,储存块内所有元素 $x \bmod{2^{k+1}}$ 的值。

1. 操作一:区间加 $v$

  • 对于完全包含在操作区间内的整块:
    • 更新 lazy += v
    • 由于 lazy 改变了,我们需要重新计算这个块的 xor_sum
    • 遍历 $k \in [0, 29]$,利用上面推导的两个区间,在第 $k$ 个排序数组中二分查找满足条件的元素个数。如果个数是奇数,则 xor_sum 的第 $k$ 位为 $1$。
    • 时间复杂度: $O(30 \log B)$。
  • 对于两端不完全包含的散块(残缺块):
    • 把该块之前的 lazy 值真实地加到块内每个元素上,并把 lazy 清零(Push Down)。
    • 把目标区间内的元素真实地加上 $v$。
    • 重构该块(Rebuild): 重新计算原数组,并重新生成这 30 个排序好的数组。
    • 时间复杂度: $O(B \log B \times 30)$(如果用常规排序)。

2. 操作二:查询区间异或和

  • 对于完全包含的整块:
    • 直接异或上该块维护好的 xor_sum
    • 时间复杂度: $O(1)$。
  • 对于两端的散块:
    • 遍历散块内的目标元素,将 (A[i] + lazy) 异或到答案中。
    • 时间复杂度: $O(B)$。

极致优化:基数排序(Radix Sort)重构技巧

上述做法中,散块重构的时间复杂度 $O(30 \cdot B \log B)$ 会成为性能瓶颈。 其实,我们可以利用基数排序的思想,将重构复杂度优化到 $O(30 \cdot B)$: 当我们求第 $k+1$ 位的排序数组时,实际上不需要用 std::sort 重新排。由于第 $k$ 位的数组已经按低 $k$ 位排好序了,我们只需要遍历第 $k$ 位的数组,把第 $k$ 位是 $0$ 的元素放在前面,第 $k$ 位是 $1$ 的元素放在后面(Stable Partition 稳定划分),就能直接得到第 $k+1$ 位的完美排序数组!

C++ 核心代码模板

下面是这个算法的精简核心代码,使用了基数排序优化,实测速度极快:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
#include <iostream>
#include <vector>
#include <cmath>
#include <algorithm>
#include <numeric>

using namespace std;

const int MAX_BITS = 30; // 通常题目范围在 1e9 以内,30位足够

struct Block {
    int l, r;
    long long lazy;
    long long xor_sum;
    vector<int> sorted_vals[MAX_BITS];
    vector<long long>& A;

    Block(int l, int r, vector<long long>& A) : l(l), r(r), lazy(0), A(A) {
        build();
    }

    // 重构块:使用基数排序思想,O(B * MAX_BITS)
    void build() {
        if (lazy != 0) {
            for (int i = l; i <= r; ++i) A[i] += lazy;
            lazy = 0;
        }
        int len = r - l + 1;
        vector<int> idx(len);
        iota(idx.begin(), idx.end(), 0);

        xor_sum = 0;
        for (int i = l; i <= r; ++i) xor_sum ^= A[i];

        for (int k = 0; k < MAX_BITS; ++k) {
            vector<int> next_idx(len);
            int zeros = 0;
            for (int i : idx) {
                if (((A[l + i] >> k) & 1) == 0) zeros++;
            }
            int p0 = 0, p1 = zeros;
            for (int i : idx) {
                if ((A[l + i] >> k) & 1) next_idx[p1++] = i;
                else next_idx[p0++] = i;
            }
            idx = next_idx;

            sorted_vals[k].resize(len);
            long long mod_mask = (1LL << (k + 1)) - 1;
            for (int i = 0; i < len; ++i) {
                sorted_vals[k][i] = A[l + idx[i]] & mod_mask;
            }
        }
    }

    // 辅助二分函数
    int get_count(int k, long long L, long long R) {
        if (L > R) return 0;
        auto it1 = lower_bound(sorted_vals[k].begin(), sorted_vals[k].end(), L);
        auto it2 = upper_bound(sorted_vals[k].begin(), sorted_vals[k].end(), R);
        return it2 - it1;
    }

    // 整块打 Lazy Tag 并更新 XOR 和
    void add_lazy(long long v) {
        lazy += v;
        xor_sum = 0;
        for (int k = 0; k < MAX_BITS; ++k) {
            long long M = 1LL << (k + 1);
            long long H = 1LL << k;
            long long V = lazy % M;

            long long count = 0;
            count += get_count(k, max(0LL, H - V), M - 1 - V);
            count += get_count(k, max(0LL, M + H - V), M - 1);

            if (count & 1) xor_sum |= (1LL << k);
        }
    }
};

复杂度总结

设定块大小 $B = \sqrt{N}$ 左右(例如 $N=10^5$ 时,取 $B = 400 \sim 500$)。

  • 空间复杂度: $O(N \times 30)$,完全可以接受。
  • 区间加时间复杂度: $O(\frac{N}{B} \cdot 30 \log B + B \cdot 30)$ 约为 $O(\sqrt{N} \log N)$。
  • 区间查时间复杂度: $O(\frac{N}{B} + B)$ 约为 $O(\sqrt{N})$。

这是处理“区间加 + 区间异或和”的最优泛用解法,代码模块化好,且没有任何难调的指针或树形结构。