[R15D] 二维异或和

  • 难度 普及/提高-
  • 时限 1s
  • 空限 512m
  • 位运算前缀和按位计数

数据规模:1n3×1051 \le n \le 3\times 10^51q3×1051 \le q \le 3\times 10^51Ai1061 \le A_i \le 10^61LiRin1 \le L_i \le R_i \le n

思路

直接对每个询问两重枚举 i,ji,j 计算复杂度是 O(qn2)O(qn^2),无法承受。关键观察是 按位独立:计算 xyx \oplus y 时,二进制下的每一位互不影响。单独看第 kk 位,AiAjA_i \oplus A_j 在该位为 11,当且仅当 AiA_iAjA_j 在该位不同。

设区间 [L,R][L,R] 内第 kk 位为 11 的元素个数为 xx,则该位为 00 的元素个数为 (RL+1)x(R-L+1)-x。要让 AiAjA_i\oplus A_j 在第 kk 位为 11,需要 i,ji,j 在该位一 1100。有序对 (i,j)(i,j)ii11jj00x×((RL+1)x)x\times ((R-L+1)-x) 个,反向(ii00jj11)同样有 x×((RL+1)x)x\times ((R-L+1)-x) 个,合计 2×x×((RL+1)x)2\times x\times ((R-L+1)-x) 个,每个贡献 2k2^k。因此整个询问的答案为

k2×xk×((RL+1)xk)×2k.\sum_{k} 2\times x_k\times ((R-L+1)-x_k)\times 2^k.

现在的问题是高效求出 xkx_k,即区间 [L,R][L,R] 内第 kk 位为 11 的元素个数。维护前缀和 cnt[k][i]cnt[k][i] 表示前缀 [1,i][1,i] 中第 kk 位为 11 的个数,则 xk=cnt[k][R]cnt[k][L1]x_k = cnt[k][R] - cnt[k][L-1],每次询问 O(1)O(1)

由于 Ai106<220A_i \le 10^6 < 2^{20},只需枚举 2020 位。复杂度:时间 O((n+q)logV)O((n+q)\log V),空间 O(nlogV)O(n\log V)

仓颉实现

import std.env.*
import std.convert.*

main(): Int64 {
    let reader = getStdIn()
    let line1 = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
    let n = line1[0]
    let q = line1[1]
    let a = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
    // A_i <= 10^6 < 2^20, 需要 20 位 (0..19)
    let B = 20
    // cnt[b][i]: 前缀 i 中第 b 位为 1 的个数
    let cnt = Array<Array<Int64>>(B, { _ => Array<Int64>(n + 1, { _ => 0 }) })
    for (b in 0..B) {
        let mask = Int64(1) << b
        let row = cnt[b]
        for (i in 1..(n + 1)) {
            let add = if ((a[i - 1] & mask) != 0) { 1 } else { 0 }
            row[i] = row[i - 1] + add
        }
    }
    let sb = StringBuilder()
    var idx = 0
    while (idx < q) {
        let parts = reader.readln().getOrThrow().split(" ", removeEmpty: true).map({ p: String => Int64.parse(p) })
        let l = parts[0]
        let r = parts[1]
        let len = r - l + 1
        var ans = Int64(0)
        for (b in 0..B) {
            let row = cnt[b]
            let x = row[r] - row[l - 1]
            ans += 2 * x * (len - x) * (Int64(1) << b)
        }
        sb.append(ans)
        if (idx < q - 1) {
            sb.append(" ")
        }
        idx++
    }
    println(sb)
    return 0
}

要点:

  • 逐位构建前缀和:cnt[b][i]cnt[b][i] 表示 A1AiA_1\sim A_i 中第 bb 位为 11 的个数,用掩码 mask = 1 << b 判断每一位。
  • 询问时对每一位用差分 O(1)O(1) 取出区间内第 bb 位为 11 的个数 xx,累加 2×x×((RL+1)x)×2b2\times x\times ((R-L+1)-x)\times 2^b。系数 22 来自有序对:(i,j)(i,j)ii11jj00x×((RL+1)x)x\times ((R-L+1)-x) 个,反向同理,两部分配对数相同,合起来翻倍。
  • 区间长度 n3×105n\le 3\times 10^5,答案最大约 2×(1.5×105)2×2192\times (1.5\times 10^5)^2\times 2^{19},用 Int64 足够。