【算法专题】二进制求异或和

二进制

计算机里存储的数据是一大串的 \(01\) 字符,我们称为二进制。

我们需要解决的问题是:

\[\sum_{i=1}^{n}\sum_{j=1}^{n}(a_i \bigoplus a_j) \]

乍一看很简单,两个 \(for\) 循环就搞定了,那么如果数据范围是 \(10^5\) 呢?很显然会超时。所以这就需要我们使用神奇的二进制来优化一下了。

求异或和:题目链接

有一个 \(n\) 个元素的数组 \(a\) ,现在求:

\[\sum_{i=1}^{n}\sum_{j=1}^{n}(a_i \bigoplus a_j) \]

模上 \(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}\)

原式变为了:

\[\sum_{i=1}^{n}\sum_{j=1}^{n}(a_i \bigoplus a_j)\\ =\sum_{i=1}^{n}\sum_{j=1}^{n}(2^0\times bit_{i,0}+2^1\times bit_{i,1}+...+2^m\times bit_{i,m})\bigoplus (2^0\times bit_{j,0}+2^1\times bit_{j,1}+...+2^m\times bit_{j,m})\\ =\sum_{i=1}^{n}\sum_{j=1}^{n}\sum_{k=0}^{m}2^k \times (bit_{i,k} \bigoplus bit_{j,k}) \]

式子变成了这样,我们又该怎么简化呢?

我们用一个样例来解释:

// 输入样例
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\) 的每一位,把每一位的贡献相加,就是我们的答案。

用公式表示为:

\[\sum_{i=1}^n\sum_{k=0}^m2^k\times f[k][!bit_{i,k}] \]

可以注意到,公式里使用了 \(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)\)

posted @ 2024-02-11 23:59  高明y  阅读(103)  评论(0)    收藏  举报