省流:gemini 3.1 Pro 对这个经典难题提出了一个优于暴力的做法。
区间加区间异或和怎么做?
具体的,你需要维护一个整数数组,支持两种操作:
- 将一个连续区间内所有元素加上一个整数
- 查询一个连续区间内的元素的异或和
这个题有任何比较好的做法吗?
这是一个在算法竞赛(如 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$ 落在了两个连续的区间内:
- $y \in [H - V, M - 1 - V]$
- $y \in [M + H - V, M - 1]$
这意味着,如果我们把每一块内的元素按照对 $M$ 取模的值进行排序,当整块被加上 $v$ 时,我们只需要用二分查找(lower_bound / upper_bound)在这两个区间内查一下元素个数,就能在 $O(\log(\text{块大小}))$ 的时间内算出整块的异或和!
具体实现步骤
我们把数组分为大小为 $B \approx \sqrt{N \log N}$ 的块。对于每一个块,我们维护以下信息:
lazy:该块积累的区间加数值。xor_sum:该块当前的整体异或和(可以直接用于查询,做到 $O(1)$ 查整块)。- 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++ 核心代码模板
下面是这个算法的精简核心代码,使用了基数排序优化,实测速度极快:
| |
复杂度总结
设定块大小 $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})$。
这是处理“区间加 + 区间异或和”的最优泛用解法,代码模块化好,且没有任何难调的指针或树形结构。