390.小E的区间异或和题目解析
给定一个数组 a,我们需要计算所有连续子数组中任意两元素的异或值之和的总和,并将结果对 109+710^9 + 7 取模。
异或的性质
在处理这个问题时,异或操作的性质非常关键。异或(XOR)是一种位运算,对于两个数 xx 和 yy,有以下几个重要性质:
- x⊕x=0x \oplus x = 0(一个数与自己异或为0)
- x⊕0=xx \oplus 0 = x(一个数与0异或不变)
- 异或运算满足交换律与结合律,即 x⊕y=y⊕xx \oplus y = y \oplus x 且 (x⊕y)⊕z=x⊕(y⊕z)(x \oplus y) \oplus z = x \oplus (y \oplus z)
问题分解
我们需要计算所有连续子数组中任意两元素的异或值之和。直接暴力计算所有子数组和其中的每对元素的异或值显然是不高效的,特别是当数组长度 nn 较大时。我们需要寻找更高效的解法。
观察到,异或是逐位进行的,我们可以逐位计算每个二进制位在所有子数组中对异或和的贡献。具体来说,我们可以通过遍历每一位来逐步累加每个位上所有子数组的贡献,从而避免了暴力枚举所有对子的问题。
解题思路
- 逐位计算:对于每一位 kk(从0到30,因为题目中的数最大为 10910^9,对应的二进制最多需要31位),我们可以统计在所有子数组中,这一位的异或贡献。
- 贡献计算:对于每一位,我们维护一个计数器
c,用来记录当前位为0和1的元素的个数。具体做法是,对于每个元素,检查其当前位是0还是1,然后分别计算这一位在所有子数组中的贡献。 - 优化计算:通过动态更新计数器
c,我们可以在遍历数组时高效地计算出当前位的贡献。每次更新时,我们可以通过当前位的值与之前的值的差异,更新结果。
代码解析
def solution(n: int, a: list) -> int:
assert n == len(a)
mod = int(1e9 + 7) # 结果取模的常量
m = 31 # 因为最大数字是 10^9,对应的二进制最大需要 31 位
ans = 0 # 最终结果
# 遍历每一位,计算该位对结果的贡献
for k in range(m):
c = [0, 0] # 用于记录当前位为0和为1的个数
s = 0 # 当前位的贡献
# 遍历数组,计算每个元素当前位的贡献
for j, x in enumerate(a):
b = (x >> k) & 1 # 取当前元素的第k位
s = (s + c[1 - b] * (n - j)) % mod # 更新当前位的贡献
c[b] = (c[b] + j + 1) % mod # 更新计数器
# 将当前位的贡献乘以2的k次方,加入总和
ans = (ans + (s << k)) % mod
return ans
关键点分析
- 按位处理:我们对每一位进行单独处理,依次计算每一位在所有子数组的异或贡献。这样避免了直接计算所有对子,提高了效率。
- 贡献计算:对于每一位 kk,我们遍历整个数组,并通过
c计数器计算该位的贡献。在遍历数组时,当前位为1的元素会与之前位为0的元素形成异或,而异或值的贡献与子数组的长度有关。 - 计数器
c:c用来记录当前位为0和为1的元素的个数。对于当前元素,如果它的第 kk 位是1,则与之前的第 kk 位为0的元素形成异或,而这种异或贡献的个数由计数器来维护。 - 结果的累加:每一位的贡献需要乘以 2k2^k 加入最终的结果,因为每一位的异或值影响的是二进制的不同位,最终需要进行加权。
- 取模操作:由于题目要求答案对 109+710^9 + 7 取模,我们在每次计算时都要进行取模,确保结果不会超出范围。
时间复杂度分析
- 每一位的计算需要遍历整个数组,因此时间复杂度是 O(n⋅m)O(n \cdot m),其中 nn 是数组的长度,mm 是我们遍历的位数(在此题中 m=31m = 31)。
- 因此,总的时间复杂度是 O(n⋅31)=O(n)O(n \cdot 31) = O(n)。
由于 nn 最大为 10510^5,这个算法的时间复杂度是可以接受的。
示例
示例 1
输入:
solution(4, [2, 3, 1, 2])
输出:
28
示例 2
输入:
solution(3, [5, 6, 7])
输出:
10
总结
该解法通过逐位计算每一位对异或和的贡献,避免了暴力枚举所有子数组的做法,提高了效率。通过合理维护计数器和累加贡献,我们能够在 O(n)O(n) 的时间复杂度内得到正确答案。