「Sumdiv」 约数之和

GitHub跳转原题关系图返回列表

把 A^B 的约数和拆成各质因子等比和的乘积,再用 1+p^k 递推在 O(log(eB)) 内求出每段等比和,全程只需乘加取模、不依赖逆元。

OJ: roj

题目 ID: 3010

难度:提高

标签:数论质因数分解分治倍增python

创建: 2026-10-01 09:57

更新: 2026-10-07 13:50

形式化题目

给定自然数 A,BA, B(0⩽A,B⩽5×1070 \leqslant A, B \leqslant 5 \times 10^7,不同时为 00),设

S=σ(AB)=∑d∣ABdS = \sigma(A^B) = \sum_{d \mid A^B} d

为 ABA^B 的所有正约数之和,求 S mod 9901S \bmod 9901。

约定 A=0A = 0 时 S=0S = 0(00 的约数和不作数学定义,本题的评测数据即取这个约定值,见样例数据 0 50000000 输出 0)。

样例:A=2,B=3A = 2, B = 3,AB=8A^B = 8,约数为 1,2,4,81, 2, 4, 8,S=15S = 15,输出 15。

正解

思路

朴素想法:先算出 ABA^B,再枚举它的每个约数求和。样例里 A=2,B=3A = 2, B = 3 时 AB=8A^B = 8,约数 1,2,4,81, 2, 4, 8,和 1515,没问题。但 A=B=5×107A = B = 5 \times 10^7 时 ABA^B 有约 Blog⁡10A≈3.8×108B \log_{10} A \approx 3.8 \times 10^8 位十进制数字——光是把这个数存下来就超过 128 MB,枚举约数更不可能。瓶颈是 ABA^B 这个数本身的规模,所以必须找到一个根本不写出 ABA^B 的做法。

关键观察一:约数之和是乘性的。 把 AA 质因数分解为

A=p1e1p2e2⋯pkek,则AB=p1e1Bp2e2B⋯pkekB.A = p_1^{e_1} p_2^{e_2} \cdots p_k^{e_k}, \qquad \text{则} \quad A^B = p_1^{e_1 B} p_2^{e_2 B} \cdots p_k^{e_k B}.

ABA^B 的每个正约数都唯一地写成 ∏ipiai\prod_i p_i^{a_i},其中 0⩽ai⩽eiB0 \leqslant a_i \leqslant e_i B。把这些选择展开成乘积:

σ(AB)=∏i=1k(1+pi+pi2+⋯+pieiB).\sigma(A^B) = \prod_{i=1}^{k} \left( 1 + p_i + p_i^2 + \cdots + p_i^{e_i B} \right).

右边每个括号只和一个质数 pip_i、一个项数 eiB+1e_i B + 1 有关,与 ABA^B 的位数毫无关系。以 A=12=22⋅3A = 12 = 2^2 \cdot 3、B=1B = 1 为例(此时两个括号分别是 1+2+221 + 2 + 2^2 与 1+31 + 3,项数 33 和 22),1212 的约数正好是下表 3 × 2 的全部格子:

j=0j = 0(不含因子 3) j=1j = 1(含一个 3)
i=0i = 0(不含 2) 2030=12^0 3^0 = 1 2031=32^0 3^1 = 3
i=1i = 1(一个 2) 2130=22^1 3^0 = 2 2131=62^1 3^1 = 6
i=2i = 2(两个 2) 2230=42^2 3^0 = 4 2231=122^2 3^1 = 12

行下标 ii 穷举了质数 2 的指数,列下标 jj 穷举了质数 3 的指数,所以格子里恰好是 2i3j2^{i}3^{j} 形式的全部约数。按行求和得 (1+3),(2+6),(4+12)(1+3), (2+6), (4+12),总和为 (1+2+4)(1+3)=7×4=28(1+2+4)(1+3) = 7 \times 4 = 28,与直接枚举 1+2+3+4+6+12=281+2+3+4+6+12 = 28 一致——“每个约数唯一地写成 2i3j2^i 3^j”正是括号相乘的依据。本题样例 A=2,B=3A = 2, B = 3 只有一个质因子,项数 =1×3+1=4= 1 \times 3 + 1 = 4,同样是 1+2+4+8=151 + 2 + 4 + 8 = 15。

关键观察二:AA 本身很小,可以直接分解。 A⩽5×107A \leqslant 5 \times 10^7,试除到 A≈7071\sqrt{A} \approx 7071 就能完成分解,剩下的 n>1n > 1 必是质数。这一步完全避开了 ABA^B。

瓶颈转移:现在只需要求一个等比和

f(m)=1+p+⋯+pm−1(mod9901),m=eiB+1,f(m) = 1 + p + \cdots + p^{m-1} \pmod{9901}, \qquad m = e_i B + 1,

但 mm 最大可达 25×5×107+1≈1.3×10925 \times 5 \times 10^7 + 1 \approx 1.3 \times 10^9,逐项累加仍然太慢。于是要找把 mm 快速变小的递推。

关键观察三:项数翻倍只需一次乘法。 记 fm=1+p+⋯+pm−1f_m = 1 + p + \cdots + p^{m-1}、gm=pmg_m = p^m。前 mm 项与后 mm 项之间只差一个 pmp^m:

f2m=fm+pmfm=fm (1+gm),g2m=gm2.f_{2m} = f_m + p^m f_m = f_m\,(1 + g_m), \qquad g_{2m} = g_m^2 .

项数多出一项时再补上第 2m2m 项(它就是 p2mp^{2m}):

f2m+1=f2m+g2m,g2m+1=g2m⋅p.f_{2m+1} = f_{2m} + g_{2m}, \qquad g_{2m+1} = g_{2m} \cdot p .

每一步都把已经确定的项数从 mm 翻倍到 2m2m,或在翻倍后加一——这正好与 mm 的二进制位对应:从最高位读到最低位,每读一位先做 m→2mm \to 2m 的翻倍,读到 1 再补一项。于是 mm 的二进制位数(最多 31 位)就是轮数,O(log⁡m)O(\log m) 完成。

以 p=3p = 3、m=5=(101)2m = 5 = (101)_2 为例:

已读的二进制位 项数 mm fmf_m(等比和) gm=3mg_m = 3^m
起始 00 00 11
1 11 11 33
10 22 4=1+34 = 1+3 99
101 55 121=1+3+9+27+81121 = 1+3+9+27+81 243243

每一行的 ff 都是把上一行翻倍(10 行:1×(3+1)=41 \times (3+1) = 4)、再按当前位是否为 1 决定要不要补一项(101 行:4×(9+1)=404 \times (9+1) = 40,再补第 4 项 8181 得 121121)得到的;gg 始终是 pp 的当前次幂。最后一行 f=121f = 121 正是 1+3+9+27+811+3+9+27+81,说明这条递推确实一次不落地累加了全部 mm 项。

为什么不用 (pk−1)/(p−1)(p^k-1)/(p-1):那个公式要除以 p−1p-1,只有当 gcd⁡(p−1,9901)=1\gcd(p-1, 9901) = 1 时才能在模意义下求逆元。只要 p≡1(mod9901)p \equiv 1 \pmod{9901}(例如 A=59407=6×9901+1A = 59407 = 6 \times 9901 + 1),p−1p - 1 就是模数的倍数,逆元不存在,除法版公式会直接失效。上面的递推只用乘法、加法和取模,对任意 pp、任意项数都成立。

算法流程:A=0A = 0 直接输出 0;否则分解 AA,对每个 (pi,ei)(p_i, e_i) 用位递推求出项数 eiB+1e_i B + 1 的等比和(pip_i 先对 9901 取模),再把所有结果相乘取模输出。

代码

Python 版:

python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 09:57
# update_at: 2026-10-01 09:57

import sys
from collections.abc import Iterator

MOD = 9901  # 题面要求的取模常量


def factors(n: int) -> Iterator[tuple[int, int]]:
    """分解 n = ∏ p^e,依次产出 (p, e)。"""
    small = [p for p in (2, 3, 5) if n % p == 0]  # 先摘掉小质因子,省去开头的试探
    for p in small:
        e = 0
        while n % p == 0:
            n //= p
            e += 1
        yield p, e
    d = 7  # 已排除 2、3、5,剩下的质因子都落在 6k±1 上
    while d * d <= n:
        for p in (d, d + 4):  # 6k+1 与 6k+5 构成一对候选
            if n % p == 0:
                e = 0
                while n % p == 0:
                    n //= p
                    e += 1
                yield p, e
        d += 6
    if n > 1:  # 除完小因子后剩下的必然是质数
        yield n, 1


def divsum(p: int, terms: int) -> int:
    """等比数列 1+p+…+p^(terms-1) 对 9901 取模:按 terms 的二进制从高位往低位折半。"""
    f, g = 0, 1  # f = 1+p+…+p^(k-1),g = p^k;k 是已经确定的项数
    for bit in bin(terms)[2:]:
        f, g = f * (g + 1) % MOD, g * g % MOD  # 项数翻倍:两段长度 k 的等比串首尾相接
        if bit == "1":  # 二进制这一位是 1:末尾再补一项 p^(2k)
            f, g = (f + g) % MOD, g * p % MOD
    return f


def sum_of_divisors(a: int, b: int) -> int:
    """求 A^B 的所有约数之和 mod 9901。"""
    if a == 0:  # 0 被所有正整数整除,约数和不是有限值;本题数据约定答案为 0
        return 0
    ans = 1
    for p, e in factors(a):  # A^B = ∏ p^(e·b),各质因子的等比和彼此独立
        ans = ans * divsum(p % MOD, e * b + 1) % MOD  # 等比和共有 e·b+1 项
    return ans


def solve() -> None:
    a, b = map(int, sys.stdin.buffer.read().split())
    print(sum_of_divisors(a, b))


if __name__ == "__main__":
    solve()

C++ 版(同一算法):

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-10-06 13:44
 * update_at: 2026-10-06 13:44
 */
#include <bits/stdc++.h>
using namespace std;

typedef long long ll;

const ll MOD = 9901; // 题面要求的取模常量

// 求等比和 1 + p + ... + p^(terms-1) 对 MOD 取模。
// 按 terms 的二进制从高位读到低位:每读一位先让项数翻倍,再决定是否补一项。
ll divsum(ll p, ll terms) {
    ll f = 0; // f = 1 + p + ... + p^(k-1),k 是已经确定的项数
    ll g = 1; // g = p^k
    for (ll bit = 1LL << 40; bit > 0; bit >>= 1) {
        f = f * (g + 1) % MOD; // 项数翻倍:两段长度为 k 的等比串首尾相接
        g = g * g % MOD;
        if (terms & bit) {     // 当前位是 1:末尾再补一项 p^(2k)
            f = (f + g) % MOD;
            g = g * p % MOD;
        }
    }
    return f;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    ll a, b;
    cin >> a >> b;

    if (a == 0) { // 本题数据约定:0 的约数和取 0
        cout << 0 << "\n";
        return 0;
    }

    // 分解 A = ∏ p^e,对每个质因子的等比和相乘
    ll ans = 1;
    ll n = a;
    for (ll p = 2; p * p <= n; p++) {
        if (n % p != 0) continue;
        ll e = 0;
        while (n % p == 0) {
            n /= p;
            e++;
        }
        ans = ans * divsum(p % MOD, e * b + 1) % MOD; // 该括号共有 e*B+1 项
    }
    if (n > 1) { // 除完小因子后剩下的部分本身就是质数,指数为 1
        ans = ans * divsum(n % MOD, b + 1) % MOD;
    }

    cout << ans << "\n";
    return 0;
}

代码分三层:factors 负责试除分解(先摘掉 2、3、5,再按 6k±16k \pm 1 试除,剩余部分 >1> 1 时它自己就是质数);divsum 是上面 (f,g)(f, g) 的位递推,只有乘法、加法和取模,不涉及逆元;sum_of_divisors 做乘性合并,并特判 A=0A = 0。p % MOD 在调用处完成,因此 divsum 内部不需要额外的取模保护。

复杂度

  • 时间:O(A+klog⁡(eB))O(\sqrt{A} + k \log(eB))(kk 是 AA 的不同质因子个数)。分解最多约 2.4×1032.4 \times 10^3 次试除;每个质因子的 divsum 至多 31 轮(项数 eB+1⩽1.25×109<231eB + 1 \leqslant 1.25 \times 10^9 < 2^{31});质因子个数 k⩽8k \leqslant 8(2⋅3⋅5⋅7⋅11⋅13⋅17⋅19=96996902 \cdot 3 \cdot 5 \cdot 7 \cdot 11 \cdot 13 \cdot 17 \cdot 19 = 9699690,再乘 23 就超过 5×1075 \times 10^7)。实测最坏数据 0.02 s 内出结果。
  • 空间:O(1)O(1)。factors 是生成器,不保存因子表;divsum 只维护两个小于 9901 的整数。

总结

本题的解法可以概括为三步替换:

  1. 把"算出 ABA^B 再枚举约数"换成 约数和公式 σ(AB)=∏i(1+pi+⋯+pieiB)\sigma(A^B) = \prod_i (1 + p_i + \cdots + p_i^{e_i B}),让指数只以乘积形式出现,彻底回避 10810^8 位的大数;
  2. 把每个括号的逐项累加换成 项数翻倍的折半递推 f2k=fk(1+gk)f_{2k} = f_k (1 + g_k),把 O(eB)O(eB) 降到 O(log⁡(eB))O(\log(eB));
  3. 把"除以 p−1p-1"换成纯乘加的递推,避免 p≡1(mod9901)p \equiv 1 \pmod{9901} 时逆元不存在。

前两步分别对应「数论里的乘性结构」和「分治/倍增思想」,第三步则是模板题里最容易踩的坑:凡是模意义下的除法,都要先确认分母与模数互质。