【算法专题】二进制求异或和
二进制
计算机里存储的数据是一大串的 \(01\) 字符,我们称为二进制。
我们需要解决的问题是:
乍一看很简单,两个 \(for\) 循环就搞定了,那么如果数据范围是 \(10^5\) 呢?很显然会超时。所以这就需要我们使用神奇的二进制来优化一下了。
求异或和:题目链接
有一个 \(n\) 个元素的数组 \(a\) ,现在求:
模上 \(10^9+7\) 的值。
我们首先从公式入手:
- 首先我们按数位拆分,我们令 \(bit_{i,k}\) 表示 \(a_i\) 在二进制的第 \(k\) 位值是多少。
- 那么 \(a_i=2^0\times bit_{i,0}+2^1\times bit_{i,1}+...+2^m\times bit_{i,m}\)
- 同理 \(a_j=2^0\times bit_{j,0}+2^1\times bit_{j,1}+...+2^m\times bit_{j,m}\)
原式变为了:
式子变成了这样,我们又该怎么简化呢?
我们用一个样例来解释:
// 输入样例
3
1 2 3
----------
k 2 1 0 // 位数
----------
1 -> 0 0 1 // 二进制表示
2 -> 0 1 0
3 -> 0 1 1
// 样例输出
12
我们知道异或的性质是不是相同为 \(1\) ,不同为 \(0\),不仅在数字上是这样,二进制位也是这样 。
我们所求为:\(1\bigoplus1+1\bigoplus2+1\bigoplus3+2\bigoplus1+2\bigoplus2+2\bigoplus3+3\bigoplus1+3\bigoplus2+3\bigoplus3\)
我们从第 \(0\) 位开始看:思考这个 \(1\) 的二进制表示的第 \(0\) 位 \(1\),怎么样才会对答案产生贡献。是不是当有一个 \(0\) 和 \(1\) 匹配才会对答案贡献 \(1\) 。所以在上面的样例,当 \(k\) 为 \(0\) 的时候,\(1\) 的二进制表示的第 \(k\) 位是 \(1\) ,遍历一下,只有 \(2\) 的二进制表示的第 \(k\) 位是 \(0\),所以产生的贡献为:\(2^k\times num\),这个 \(num\) 表示第 \(k\) 位是 \(0\) 的个数。
综上所述,我们遍历 \(a_i\) 然后每次遍历的时候,内层再遍历 \(a_i\) 的每一位,把每一位的贡献相加,就是我们的答案。
用公式表示为:
可以注意到,公式里使用了 \(f[k][!bit_{i,k}]\) 表示 \(a_i\) 在第 \(k\) 位时,与第 \(k\) 位匹配的数的个数。
那么这个第二维加 \(!\) 是取反的意思,就是如果 \(a_i\) 的第 \(k\) 位是 \(1\) ,那么我们与第 \(k\) 位匹配的数字是 \(0\) ,所以我们需要 \(0\) 的个数。因此我们要取个反。
那么这个数组是不是我们要先预处理出来啊。
// 预处理 f[32][2] 数组,注意数组大小第一维是数位,第二维是数位的取值
for (int i = 1; i <= n; i++)
for (int j = 0; j <= 31; j++)
f[j][(a[i] >> j) & 1]++; // 统计个数
那么有了 \(f\) 数组,我们就可以根据公式完成这道题了。
LL ans = 0;
for (int i = 1; i <= n; i++)
for (int j = 0; j <= 31; j++)
ans = (ans + (1ll << j) * f[j][((a[i] >> j) & 1) ^ 1]) % mod;
cout << ans;
还有个细节,为什么 f[j][((a[i] >> j) & 1) ^ 1]
这个还要取个异或呢?
- 因为我们想要的是相匹配的数的个数,和 \(1\) 匹配的是 \(0\) ,和 \(0\) 匹配的是 \(1\) ,所以,异或可以直接转换,如果当前这位是 \(1\),那么我们要的就是 \(0\)。
至此,时间复杂度为 \(T(32n+32n)=O(n)\)