[R11F] 二维gcd和2

  • 难度 提高
  • 时限 2s
  • 空限 1024m
  • 数学欧拉函数数论

数据规模:1N5×1071 \le N \le 5\times 10^7,答案对 998244353998244353 取模。

思路

要求 i=1Nj=1Ngcd(i,j)\sum_{i=1}^N\sum_{j=1}^N\gcd(i,j),直接枚举 i,ji,jO(N2)O(N^2),无法承受。换一个角度,按 gcd(i,j)\gcd(i,j) 的值分类贡献。

定义 s[k]s[k] 为满足 1ik1\le i\le k1jk1\le j\le kgcd(i,j)=1\gcd(i,j)=1 的二元组 (i,j)(i,j) 数量。若 gcd(i,j)=x\gcd(i,j)=x,等价于 gcd(i/x,j/x)=1\gcd(i/x,j/x)=1,所以 gcd(i,j)=x\gcd(i,j)=x 的二元组个数恰好是 s[N/x]s[\lfloor N/x\rfloor],答案即为

x=1Nxs ⁣Nx\sum_{x=1}^{N} x\cdot s\!\left\lfloor\frac{N}{x}\right\rfloor

接下来计算 s[k]s[k]。除 (1,1)(1,1) 外,其余互质对可分为 i<ji<ji>ji>j 两类。引入欧拉函数 φ(i)\varphi(i) 表示 1i1\sim i 中与 ii 互质的数的个数,则 jj 固定时 i<ji<j 且互质的 iiφ(j)\varphi(j) 个,对称地 i>ji>j 同理,于是

s[k]=1+2y=2kφ(y)s[k]=1+2\sum_{y=2}^{k}\varphi(y)

预处理出 φ\varphi 的前缀和 S[k]=y=2kφ(y)S[k]=\sum_{y=2}^{k}\varphi(y),则 s[k]=1+2S[k]s[k]=1+2S[k],整个答案可在 O(N)O(N) 内累加。

算法瓶颈在于预处理欧拉函数。 用埃氏筛可以在 O(NloglogN)O(N\log\log N) 内求出所有 φ\varphi:初始化 φ(i)=i\varphi(i)=i,对每个素数 pp,把它所有倍数 jjφ(j)\varphi(j) 乘以 (11/p)(1-1/p),即 φ(j)φ(j)/p(p1)\varphi(j)\leftarrow \varphi(j)/p\cdot(p-1)

复杂度:时间 O(NloglogN)O(N\log\log N),空间 O(N)O(N)

仓颉实现

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

main(): Int64 {
    let MOD: Int64 = 998244353
    let reader = getStdIn()
    let n = Int64.parse(reader.readln().getOrThrow())
    // 埃氏筛欧拉函数 phi[1..n],用 UInt32 节省内存,无需额外素数表
    // 初始化 phi[i]=i,对每个素数 i,把它的所有倍数 j 的 phi[j] *= (1-1/i) = phi[j] - phi[j]/i
    var phi = Array<UInt32>(n + 1, { x: Int64 => UInt32(x) })
    var i: Int64 = 2
    while (i <= n) {
        if (Int64(phi[i]) == i) {
            // i 是素数
            var j = i
            while (j <= n) {
                phi[j] = UInt32(Int64(phi[j]) / i * (i - 1))
                j += i
            }
        }
        i += 1
    }
    // 原地将 phi[k] 转为前缀和 phisum[k] = sum_{y=2}^k phi[y] % MOD(< MOD 可存 UInt32)
    // s[k] = 1 + 2*phisum[k] 为 1..k 中互质对 (i,j) 数量
    var run: Int64 = 0
    var k: Int64 = 1
    while (k <= n) {
        if (k >= 2) {
            run = (run + Int64(phi[k])) % MOD
        }
        phi[k] = UInt32(run)
        k += 1
    }
    // 答案 = sum_{x=1}^n x * s[floor(n/x)]
    // x 与 s 都 < MOD,直接相乘会溢出 Int64,先 (x % MOD) * sval % MOD
    var ans: Int64 = 0
    var x: Int64 = 1
    while (x <= n) {
        let q = n / x
        let sval = (1 + 2 * Int64(phi[q])) % MOD
        ans = (ans + (x % MOD) * sval) % MOD
        x += 1
    }
    println(ans)
    return 0
}

要点:

  • gcd\gcd 的值分类:gcd(i,j)=x\gcd(i,j)=x 的对数就是 N/x\lfloor N/x\rfloor 范围内互质对数 s[N/x]s[\lfloor N/x\rfloor],把 O(N2)O(N^2) 降维到对 xxO(N)O(N) 求和。
  • 互质对数用欧拉函数前缀和表示:s[k]=1+2y=2kφ(y)s[k]=1+2\sum_{y=2}^{k}\varphi(y),关键是把 (1,1)(1,1) 单独算,其余按 i<ji<ji>ji>j 对称翻倍。
  • 欧拉函数用埃氏筛 O(NloglogN)O(N\log\log N) 求出,利用 φ\varphi 是积性函数,对素数 pp 的倍数 jj 执行 φ(j)φ(j)/p(p1)\varphi(j)\leftarrow\varphi(j)/p\cdot(p-1)
  • 内存优化NN 最大 5×1075\times 10^7,数组用 UInt32φ(i)i1<5×107\varphi(i)\le i-1<5\times 10^7 且取模后 <998244353<998244353 都能存下),把 400MB400\,\text{MB} 压缩到 200MB200\,\text{MB},并省去单独的素数表,避免内存超限。
  • 防溢出:前缀和与答案累加全程对 MODMOD 取模;计算 xsx\cdot s 时先 x % MOD 再相乘,避免 Int64 溢出;筛法中的乘法通过 Int64(phi[j]) 临时提升精度后再写回 UInt32