跳转至

第 83 章 整除分块与数论进阶

配套例题:BISHI41 【模板】整除分块、BISHI42 余数求和、 BISHI39 【模板】Pollard-Rho 算法、BISHI40 数组取精 来源:S3 day10《数论与组合数学》(周尚彦)第 17–20、28–33 页; day9 resource《数论》(高铭鸿)第 4–6 页、《数论》(邹雨恒)第 21–26、80–106、133–138 页、 《数论与信息学竞赛》(胡渊鸣)第 19–21、38 页、《数论》(李有为)第 16–25 页 前置80-数论基础82-欧拉函数与欧拉降幂

这一章把三件「进阶但常考」的事讲清楚:

  1. 整除分块——把 \(\sum_{i=1}^n f(\lfloor n/i\rfloor)\)\(O(n)\) 降到 \(O(\sqrt n)\)
  2. Miller-Rabin / Pollard-Rho——大数的素性判定与分解;
  3. 莫比乌斯反演——把 \([\gcd(a,b)=1]\) 这种条件拆成可交换求和的形式。

Python 在这一章有一处巨大优势:Miller-Rabin 和 Pollard-Rho 在 C++ 里的一大半代码 都是在跟 long long 溢出搏斗(龟速乘、__int128、Montgomery 模乘), 而 Python 的 int 任意精度,这些全都不需要写。


83.1 整除分块(数论分块)

核心事实

高铭鸿的课件把它列成「有关整除的一些小性质」的第一条:

\(\lfloor n/i \rfloor\) 的不同结果数是 \(O(\sqrt n)\) 的。

证明:分两段看。 - \(i \le \sqrt n\):这样的 \(i\) 至多 \(\sqrt n\) 个,所以贡献至多 \(\sqrt n\) 个不同值; - \(i > \sqrt n\):此时 \(\lfloor n/i \rfloor < \sqrt n\),取值只能落在 \([0, \sqrt n)\), 同样至多 \(\sqrt n\) 个。

合计至多 \(2\sqrt n\) 个不同值。\(\square\)

周尚彦的课件给的提示是同一件事的另一个说法:

\(k \times \lfloor n/k \rfloor \le n\),其中必有 \(k \le \sqrt n\)\(\lfloor n/k \rfloor \le \sqrt n\)

块的右端点

引理:设 \(v = \lfloor n/l \rfloor\)\(v \ge 1\)),则使 \(\lfloor n/r \rfloor = v\)最大 \(r\) 是 $\(r = \left\lfloor \frac{n}{v} \right\rfloor\)$

证明\(\lfloor n/r \rfloor = v \iff v \le n/r < v+1 \iff \frac{n}{v+1} < r \le \frac{n}{v}\)。 因为 \(r\) 是整数,最大的 \(r\) 就是 \(\lfloor n/v \rfloor\)\(\square\)

于是可以「整块整块地跳」:l = 1 开始,每次算 v = n // lr = n // v, 处理 \([l, r]\) 这一整段,然后 l = r + 1

模板一:整除分块

# [片段] 模板:整除分块,枚举所有 (l, r, v) 三元组
def blocks(n, limit=None):
    """生成 (l, r, v):区间 [l, r] 内 n//i 恒等于 v。共 O(sqrt n) 段。"""
    # 上界必须收到 min(n, limit):一旦 l > n 就有 v = n//l == 0,下一行会除零
    m = n if limit is None else min(n, limit)
    l = 1
    while l <= m:
        v = n // l                  # 本段的公共商,由左端点决定
        # floor(n/r) == v 等价于 n/(v+1) < r <= n/v,所以最大的 r 就是 floor(n/v)。
        # 这一行是整除分块的全部:把「逐个 i」变成「整段 [l, r] 一起处理」
        r = min(m, n // v)          # ★ 右端点要对 limit 取 min
        yield l, r, v
        l = r + 1                   # 跳到下一段的左端点,商必然变小


def sum_floor(n):
    """Σ_{i=1..n} floor(n/i),O(sqrt n)。"""
    total = 0
    l = 1
    while l <= n:
        v = n // l                  # l <= n 保证 v >= 1,n // v 不会除零
        r = n // v                  # 同值区间 [l, r] 的右端点
        total += v * (r - l + 1)    # 这一段有 r-l+1 项,每项都等于 v
        l = r + 1
    return total

两个常见变体

求和式 每块的贡献
\(\sum \lfloor n/i \rfloor\) \(v \times (r-l+1)\)
\(\sum i \cdot \lfloor n/i \rfloor\) \(v \times \frac{(l+r)(r-l+1)}{2}\)
\(\sum f(i)\lfloor n/i\rfloor\) \(v \times (F(r)-F(l-1))\)\(F\)\(f\) 的前缀和
二维 \(\sum \lfloor n/i\rfloor\lfloor m/i\rfloor\) 右端点取 \(\min(n//(n//l),\ m//(m//l))\)

v = n // lv 一定 \(\ge 1\)(因为 \(l \le n\)),所以 n // v 不会除零。 但若循环上界是 \(m > n\)(比如 BISHI42 里 \(n > k\) 的情形), 就必须先把上界收到 \(\min(n, m)\),否则 n // l == 0ZeroDivisionError

一个更快的等价写法:数格点

\(\sum_{i=1}^{n}\lfloor n/i\rfloor\) 这个最经典的式子,还有一个常数更小的写法。

注意到 \(\lfloor n/i \rfloor\) 就是「\(i\) 的倍数中不超过 \(n\) 的个数」,所以

\[\sum_{i=1}^{n}\left\lfloor \frac ni \right\rfloor = \#\{(i,j) : i,j \ge 1,\ ij \le n\}\]

双曲线 \(ij=n\) 下方的格点数。以 \(s = \lfloor\sqrt n\rfloor\) 为界做容斥 (这就是数论里的「双曲线法 / Dirichlet 双曲线技巧」):

\[\sum_{i=1}^{n}\left\lfloor \frac ni \right\rfloor = 2\sum_{i=1}^{s}\left\lfloor \frac ni \right\rfloor - s^2\]

证明:把格点分成「\(i \le s\)」和「\(j \le s\)」两块。 每块各有 \(\sum_{i=1}^{s}\lfloor n/i\rfloor\) 个格点(第二块按 \(j\) 数)。 两块的交集是 \(\{(i,j): i\le s, j\le s\}\) 里满足 \(ij\le n\) 的格点—— 而 \(i,j\le s\)\(ij \le s^2 \le n\) 自动成立,所以交集恰是 \(s\times s\) 的完整正方形。 并集的元素恰好覆盖全部满足 \(ij\le n\) 的格点(若 \(i>s\)\(j>s\)\(ij>n\))。 由容斥即得。\(\square\)

为什么在 Python 里更划算:循环体只有一次整除加一次加法, 可以直接写成 sum(n // i for i in range(1, s + 1)) 让求和落到 C 层; 而整除分块的循环体有 4–5 个操作且无法向量化。


83.2 约数个数与约数和

\(n = \prod p_i^{a_i}\)

\[d(n) = \prod (a_i+1), \qquad \sigma(n) = \prod \frac{p_i^{a_i+1}-1}{p_i-1}\]

(推导见 80 章。陈许旻的课件把这两条列在「因子个数」一节。)

前缀和:用整除分块

\[\sum_{i=1}^{n} d(i) = \sum_{i=1}^{n}\left\lfloor \frac ni \right\rfloor, \qquad \sum_{i=1}^{n} \sigma(i) = \sum_{i=1}^{n} i\left\lfloor \frac ni \right\rfloor\]

证明:交换求和顺序。 \(\sum_{i\le n} d(i) = \sum_{i\le n}\sum_{d\mid i} 1 = \sum_{d\le n}\#\{i\le n : d\mid i\} = \sum_{d\le n}\lfloor n/d\rfloor\)。 约数和同理,只是每个约数 \(d\) 贡献 \(d\) 而不是 1。\(\square\)

这解释了 BISHI41 为什么值得单独出成模板题\(\sum\lfloor n/i\rfloor\) 既是格点计数, 也是约数个数的前缀和,还是整除分块的最小实例。

\(1..n\) 每个数的约数个数 / 约数和

不用分解,直接倍数枚举(调和级数 \(O(n\log n)\)):

# [片段] 模板:O(n log n) 求 1..n 的约数个数表与约数和表
def divisor_tables(n):
    d = [0] * (n + 1)                # 约数个数
    s = [0] * (n + 1)                # 约数和
    # 不做分解,改成「枚举约数 i,去更新它的所有倍数 j」:
    # 内层次数是 n/i,全部加起来是调和级数 n·ln n
    for i in range(1, n + 1):
        for j in range(i, n + 1, i): # i 是 j 的约数
            d[j] += 1                # j 多了一个约数 i
            s[j] += i                # 约数和累加这个约数本身
    return d, s

Python 现实性:内层总迭代次数是 \(n\ln n\)\(n=10^6\) 时约 \(1.4\times10^7\) 次, 约 4 秒——危险\(n \le 2\times10^5\) 才稳。 若只需要约数个数,可以改用线性筛(\(d\) 是积性函数),但常数同样不小。 能用整除分块只求前缀和的,就不要把整张表建出来。

枚举单个数的全部约数

# [片段] 模板:O(sqrt n) 枚举 n 的全部约数
import math


def divisors(n):
    """返回 n 的全部约数(无序)。O(sqrt n)。"""
    res = []
    # 约数成对出现:i 与 n//i 中必有一个不超过 sqrt(n),所以只枚举小的那半
    for i in range(1, math.isqrt(n) + 1):
        if n % i == 0:
            res.append(i)            # 小的那个
            if i != n // i:          # ★ 完全平方数时别把 sqrt 算两次
                res.append(n // i)   # 配对的大的那个
    return res

83.3 Miller-Rabin 素性测试

试除法是 \(O(\sqrt n)\)\(n = 10^{18}\) 时不可行;询问次数多时连 \(n=10^{12}\) 都撑不住。

从费马测试到 Miller-Rabin

费马测试:若 \(n\) 是质数且 \(\gcd(a,n)=1\),则 \(a^{n-1}\equiv1\pmod n\)。 反过来用:随机取 \(a\),若 \(a^{n-1}\not\equiv1\),则 \(n\) 一定是合数。

胡渊鸣的课件指出了它的问题:

反例:\(2^{340}\equiv 1 \pmod{341}\)。反例确实不多, 但是不能忽略。

\(341 = 11\times31\)。更糟的是 Carmichael 数,如 \(561\),对所有与之互质的底数都通过费马测试。)

加强的依据是二次探测定理

二次探测定理\(p\) 是奇质数,若 \(x^2 \equiv 1 \pmod p\),则 \(x \equiv \pm1 \pmod p\)

证明\(p \mid (x-1)(x+1)\)\(p\) 是质数所以 \(p\mid(x-1)\)\(p\mid(x+1)\)\(\square\)

于是把 \(n-1\) 写成 \(d\cdot2^s\)\(d\) 为奇数),从 \(a^d\) 开始不断平方, 最终要到达 \(a^{n-1}\equiv1\)。如果 \(n\) 是质数,这条平方链第一次变成 1 之前的那一项必须是 \(-1\)

Miller-Rabin 判定:对底数 \(a\)\(n\) 通过测试当且仅当 $\(a^{d}\equiv1 \pmod n \quad\text{或}\quad \exists\, 0\le r<s,\ a^{d\cdot2^r}\equiv-1\pmod n\)$ 否则 \(n\) 必为合数。

单个底数的误判概率 \(\le 1/4\)\(k\) 个随机底数误判概率 \(\le 4^{-k}\)

确定性底数:竞赛里不要用随机

已知的确定性结论(可以直接背):

\(n\) 的上界 底数集合
\(3.2\times10^9\)\(2^{32}\) 级) \(\{2, 3, 5, 7\}\)
\(3.47\times10^{12}\) \(\{2, 3, 5, 7, 11, 13\}\)
\(3.4\times10^{14}\) \(\{2,3,5,7,11,13,17\}\)
\(3.3\times10^{24}\)(含全部 \(2^{64}\) 前 13 个质数 \(\{2,3,\ldots,41\}\)
\(< 2^{64}\)(更快) \(\{2, 325, 9375, 28178, 450775, 9780504, 1795265022\}\)(7 个)

(胡渊鸣课件提到的 \(\{2,3,7,61,24251\}\)\(10^{16}\) 内只有唯一强伪素数 \(46856248255981\), 所以那组底数不能直接用,要额外特判——用上表更省心。)

为什么竞赛里坚持确定性底数? 随机底数意味着同一份代码同一组数据可能这次 AC 下次 WA, 调试时无法复现。确定性底数让程序完全可预测。

模板二:Miller-Rabin

# [片段] 模板:Miller-Rabin,确定性底数,适用于 n < 3.3e24
_SMALL = (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41)


def is_prime(n):
    """确定性 Miller-Rabin。Python 的 pow(a,d,n) 是 C 层模幂,不需要龟速乘。"""
    if n < 2:
        return False
    # 先用小质数试除:既筛掉绝大多数合数,也保证后面 a % n != 0
    for p in _SMALL:
        if n % p == 0:
            return n == p            # 小质数本身算质数,其余倍数直接判负
    # 把 n-1 的因子 2 全部提出来,得到 n - 1 = d * 2^s(d 为奇数)。
    # 二次探测就沿着 a^d, a^(2d), a^(4d), ... 这条平方链检查
    d = n - 1
    s = 0
    while d % 2 == 0:                # n - 1 = d * 2^s,d 为奇数
        d //= 2
        s += 1
    for a in _SMALL:                 # n < 3.3e24 时这 13 个底数是确定性的
        x = pow(a, d, n)             # ★ 内置快速幂,C 实现
        if x == 1 or x == n - 1:
            continue                 # 链首就是 ±1,这个底数不能证伪,换下一个
        # 还剩 s-1 次平方机会:最后一次平方得到的是 a^(n-1),不必再看
        for _ in range(s - 1):
            x = x * x % n            # ★ Python 不会溢出,C++ 这里要 __int128
            if x == n - 1:
                break                # 出现 -1,符合质数应有的形态
        else:
            # for 的 else 只在没 break 时执行:整条链都没出现 -1,
            # 却最终会平方到 1,违反二次探测定理
            return False             # 平方链里没出现 -1 -> 合数
    return True

for ... else 的用法else 分支只在循环没有 break 时执行。 这里正好表达「整条平方链都没出现 \(-1\)」。 见 10-条件与循环

Python 相对 C++ 的三处便宜

C++ 要做的事 Python
\(n>10^{18}\)a*b%n 溢出 → 龟速乘 / __int128 / Montgomery a * b % n 直接写
手写快速幂 pow(a, d, n)
随机数生成器、种子管理 确定性底数,一个随机数都不用

83.4 Pollard-Rho 分解

试除法分解是 \(O(\sqrt n)\);Pollard-Rho 期望 \(O(n^{1/4})\)。 李有为的课件一句带过:「用 Pollard rho 可以做到 \(O(n^{1/4})\) 分解」。

原理(生日悖论):取伪随机函数 \(f(x) = (x^2+c)\bmod n\),从 \(x_0\) 迭代得到序列。 设 \(p\)\(n\) 的一个质因子,那么这个序列\(p\) 的周期期望是 \(O(\sqrt p) = O(n^{1/4})\)。 一旦出现 \(x_i \equiv x_j \pmod p\)\(x_i \ne x_j\)\(\gcd(|x_i-x_j|, n)\) 就是 \(n\) 的一个非平凡因子。

用 Floyd 判圈(龟兔)或 Brent 的批量取 gcd 来找这样的 \((i,j)\)

模板三:Pollard-Rho(Brent 变体)

# [片段] 模板:Pollard-Rho 完整分解,配合上面的 is_prime 使用
import math
import random


def _pollard(n):
    """返回 n 的一个非平凡因子。要求 n 是合数且 n 不是 2 的倍数。"""
    if n % 2 == 0:
        return 2
    # 外层 while:一组 (c, 起点) 可能只找出平凡因子,失败就换一组重来
    while True:
        c = random.randrange(1, n)
        f = lambda x: (x * x + c) % n    # 伪随机迭代函数,c 是这一轮的随机偏移
        x = y = random.randrange(0, n)
        d = 1
        # Brent:把多步的差乘在一起,批量取一次 gcd,省掉大量 gcd 调用
        while d == 1:
            prod = 1
            for _ in range(128):         # 128 步为一批,批越大 gcd 调用越少
                x = f(x)                 # 龟:走一步
                y = f(f(y))              # 兔:走两步
                if x == y:
                    break                # 龟兔相遇,本轮迭代已进入环,无解
                prod = prod * abs(x - y) % n
                if prod == 0:            # 乘出 0,退回逐步取 gcd
                    break
            # 一次 gcd 覆盖整批:只要批内任一个 |x-y| 与 n 有公因子,就会被抓到
            d = math.gcd(prod, n)
            if x == y or prod == 0:
                break
        if 1 < d < n:                    # 非平凡因子才算成功,否则换 c 重来
            return d


def factorize_big(n):
    """大整数完整分解,返回 {p: e}。期望 O(n^(1/4))。"""
    res = {}
    stack = [n]                          # 用显式栈代替递归,不受递归深度限制
    while stack:
        v = stack.pop()
        if v == 1:
            continue                     # 分解到 1 就没有质因子了
        if is_prime(v):                  # 递归边界靠 Miller-Rabin 判定
            res[v] = res.get(v, 0) + 1   # 同一质因子可能被拆出多次,累加重数
            continue
        d = _pollard(v)                  # 拆出一个非平凡因子,两半继续分解
        stack.append(d)
        stack.append(v // d)
    return res

什么时候真的需要 Pollard-Rho? \(n \le 10^{12}\) 时试除只要 \(10^6\) 次循环,Python 下 0.3 秒, 单组数据用试除就够。只有「\(T\) 组 × \(n \ge 10^{14}\) 要分解」才必须上 Pollard-Rho。 牛客这道 BISHI39 虽然挂着 Pollard-Rho 的名字,实际只要 Miller-Rabin(见例题)。


83.5 莫比乌斯函数与反演

定义

\[\mu(n) = \begin{cases} 1 & n = 1\\ (-1)^k & n = p_1p_2\cdots p_k\ (\text{互不相同的质数})\\ 0 & \exists\, p,\ p^2 \mid n \end{cases}\]

邹雨恒的课件一句话点破本质:

本质上是一种容斥原理所产生的系数

\(\mu\)积性函数\(\mu(p) = -1\)\(\mu(p^a) = 0\ (a\ge2)\)

最关键的一条式子

\[\sum_{d \mid n}\mu(d) = [n=1]\]

证明\(n=1\) 时显然。\(n>1\) 时设 \(n\)不同质因子有 \(k\) 个(\(k\ge1\))。 含平方因子的 \(d\) 贡献 0,所以只需考虑「不同质因子的子集」形成的 \(d\), 选出 \(j\) 个质因子的 \(d\)\(\binom kj\) 个,各贡献 \((-1)^j\)。于是 $\(\sum_{d\mid n}\mu(d) = \sum_{j=0}^{k}\binom kj(-1)^j = (1-1)^k = 0 \qquad \square\)$

这条式子的用法(邹雨恒课件的「小总结」):

注意 \(\sum_{d\mid n}\mu(d)=[n=1]\),就能把 \([\gcd(a,b)=1]\) 转化为 \([d\mid\gcd(a,b)]\), 整除某两个数最大公约数就好做多了。

具体地:

\[\sum_{a=1}^{N}\sum_{b=1}^{M}[\gcd(a,b)=1] = \sum_{a}\sum_{b}\sum_{d\mid\gcd(a,b)}\mu(d) = \sum_{d=1}^{\min(N,M)}\mu(d)\left\lfloor\frac Nd\right\rfloor\left\lfloor\frac Md\right\rfloor\]

最后一步再套整除分块就是 \(O(\sqrt N)\)「莫比乌斯 + 整除分块」是这类题的标准组合拳。

莫比乌斯反演

定理:若 \(F(n) = \sum_{d\mid n} f(d)\),则 \(f(n) = \sum_{d\mid n}\mu\!\left(\frac nd\right)F(d)\)

证明: $\(\sum_{d\mid n}\mu\left(\frac nd\right)F(d) = \sum_{d\mid n}\mu\left(\frac nd\right)\sum_{e\mid d} f(e) = \sum_{e\mid n} f(e)\sum_{d:\ e\mid d\mid n}\mu\left(\frac nd\right)\)$ 内层令 \(d = e t\),则 \(t \mid \frac ne\),内层和为 \(\sum_{t\mid n/e}\mu\left(\frac{n/e}{t}\right) = [n/e=1]\)。 于是只剩 \(e=n\) 一项,即 \(f(n)\)\(\square\)

与欧拉函数的对偶

同一套手法也可以用 \(\varphi\)(见 82 章\(\sum_{d\mid n}\varphi(d)=n\))。 邹雨恒课件的经典推导:

\[\sum_{a=1}^{N}\sum_{b=1}^{M}\gcd(a,b) = \sum_{a}\sum_{b}\sum_{d\mid\gcd(a,b)}\varphi(d) = \sum_{d=1}^{\min(N,M)}\varphi(d)\left\lfloor\frac Nd\right\rfloor\left\lfloor\frac Md\right\rfloor\]

记忆法\([\gcd=1]\)\(\mu\)\(\gcd\) 本身用 \(\varphi\)

模板四:线性筛莫比乌斯函数

# [片段] 模板:线性筛同时求 μ 与素数表,O(n)
def sieve_mu(n):
    """返回 (primes, mu),mu[i] = μ(i),i = 0..n。"""
    mu = [0] * (n + 1)
    mu[1] = 1                            # μ(1) = 1,空的质因子集合
    primes = []
    is_comp = bytearray(n + 1)           # 1 字节一格,比 list 省 8 倍内存
    for i in range(2, n + 1):
        if not is_comp[i]:               # 没被任何更小的质数筛掉 -> i 是质数
            primes.append(i)
            mu[i] = -1                   # μ(p) = -1
        # 用 i 乘上「不超过 i 的最小质因子」的那些质数去筛,保证每个合数
        # 只被它的最小质因子筛掉一次,总次数是 O(n)
        for p in primes:
            v = i * p
            if v > n:
                break                    # 越界,后面的 p 只会更大
            is_comp[v] = 1               # v 的最小质因子就是 p
            if i % p == 0:
                # p 已经是 i 的质因子,v = i*p 含 p²,μ(v) = 0;
                # 同时这个 break 保证再往后的质数不会重复筛到同一个合数
                mu[v] = 0                # p² | v -> μ = 0
                break
            mu[v] = -mu[i]               # 多一个不同质因子,符号取反
    return primes, mu

83.6 狄利克雷卷积入门

定义\((f * g)(n) = \sum_{d\mid n} f(d)\, g\!\left(\frac nd\right)\)

几个基本函数:

记号 定义
\(\varepsilon(n)\) \([n=1]\)(卷积单位元)
\(\mathbf1(n)\) 恒为 1
\(\operatorname{id}(n)\) \(n\)
\(\mu, \varphi, d, \sigma\) 见前文

必须记住的几条卷积恒等式

恒等式 等价的经典式子
\(\mu * \mathbf1 = \varepsilon\) \(\sum_{d\mid n}\mu(d)=[n=1]\)
\(\varphi * \mathbf1 = \operatorname{id}\) \(\sum_{d\mid n}\varphi(d)=n\)
\(\mathbf1 * \mathbf1 = d\) 约数个数
\(\operatorname{id} * \mathbf1 = \sigma\) 约数和
\(\varphi = \mu * \operatorname{id}\) 由前两条 + 结合律推出

性质(邹雨恒课件):

  • 狄利克雷卷积满足交换律、结合律,对加法有分配律;
  • 两个积性函数的狄利克雷卷积仍是积性函数
  • 莫比乌斯反演就是「两边卷 \(\mu\)」:\(F = f * \mathbf1 \iff f = F * \mu\)

为什么这套语言有用:把「\(\sum_{d\mid n}\) 型恒等式」变成代数运算之后, 推导可以像解方程一样做。比如要证 \(\varphi = \mu * \operatorname{id}\)\(\varphi * \mathbf1 = \operatorname{id}\),两边卷 \(\mu\), 得 \(\varphi * (\mathbf1 * \mu) = \operatorname{id} * \mu\),即 \(\varphi * \varepsilon = \mu * \operatorname{id}\)\(\square\)

进阶:杜教筛能在 \(O(n^{2/3})\) 内求 \(\mu\)\(\varphi\) 的前缀和, 核心是邹雨恒课件最后给的式子 \(G(n) = \sum_{i=1}^{n} F(\lfloor n/i\rfloor)\)—— 它本身又是整除分块。本教程不展开。


83.7 例题

BISHI41 【模板】整除分块(中等)

给定 \(1 \le n \le 10^{12}\),求 \(\sum_{i=1}^{n}\lfloor n/i\rfloor\)。 题面见 BISHI41 原题(牛客)

两条路都是 \(O(\sqrt n)\):整除分块,或 83.1 末尾的数格点容斥。 本题解取后者,因为循环体更短、可以整个交给 C 层的 sum

\[\text{答案} = 2\sum_{i=1}^{s}\left\lfloor\frac ni\right\rfloor - s^2,\qquad s=\lfloor\sqrt n\rfloor\]
import sys
from math import isqrt

n = int(sys.stdin.buffer.read().split()[0])
s = isqrt(n)          # 精确整数开方;n 到 1e12 时 int(n ** 0.5) 可能差 1
# 数格点 (i, j) 且 i*j <= n:两块各算一次,重叠的 s*s 减掉。
# 生成器交给内置 sum,1e6 次迭代全在 C 层,比整除分块的循环体更省
print(2 * sum(n // i for i in range(1, s + 1)) - s * s)

验算 \(n=10\)\(s=3\)\(\sum_{i=1}^{3}\lfloor10/i\rfloor = 10+5+3=18\)\(2\times18-9=27\); 手算 \(10+5+3+2+2+1+1+1+1+1=27\)

三个坑

  1. math.isqrt 而非 int(n ** 0.5)\(n\)\(10^{12}\) 时浮点开方可能差 1, 容斥的正方形边长就错了,答案会偏差 \(O(\sqrt n)\)
  2. 容斥减掉的是 \(s^2\)(重叠的正方形),不是 \(s\)
  3. \(n=1\)\(s=1\)\(2\times1-1=1\) ✓,边界自然成立。

规模\(\sqrt{10^{12}} = 10^6\) 次 C 层迭代,约 0.1–0.2 秒。 答案量级 \(n\ln n \approx 2.8\times10^{13}\),C++ 要 long long,Python 无忧。

题解见 solutions/BISHI41.py

BISHI42 余数求和(中等)

给定 \(1 \le n, k \le 10^9\),求 \(\sum_{i=1}^{n}(k \bmod i)\)。 题面见 BISHI42 原题(牛客)

第一步永远是把取模拆开(周尚彦课件的原话:「按照取模定义式把原式改写」):

\[k \bmod i = k - i\left\lfloor\frac ki\right\rfloor \quad\Longrightarrow\quad \sum_{i=1}^{n}(k\bmod i) = nk - \sum_{i=1}^{n} i\left\lfloor\frac ki\right\rfloor\]

\(i > k\)\(\lfloor k/i\rfloor = 0\),所以右边那个和只需算到 \(m=\min(n,k)\)。 再用整除分块,每块贡献 \(v\cdot\frac{(l+r)(r-l+1)}{2}\)

import sys


def main():
    n, k = map(int, sys.stdin.buffer.read().split()[:2])
    m = min(n, k)                       # i > k 的项 floor(k/i) = 0,不必进循环
    total = 0                           # 累加 Σ i * floor(k/i)
    l = 1
    while l <= m:
        v = k // l                      # l <= m <= k 保证 v >= 1,下一行不会除零
        # 由 floor(k/r) = v 的最大 r 是 floor(k/v);再对 m 取 min 防止越出求和上界
        r = min(m, k // v)              # [l, r] 内 floor(k/i) 恒等于 v
        # 这一段的贡献是 v 乘上 l..r 的等差和;`// 2` 必须是整除,
        # 用 `/ 2` 会转成浮点,5e17 量级直接丢精度
        total += v * (l + r) * (r - l + 1) // 2
        l = r + 1                       # 跳到下一段
    # k mod i = k - i*floor(k/i),对 i = 1..n 求和即 n*k - total
    print(n * k - total)


main()

验算样例 \(n=10, k=5\)\(nk=50\)\(\sum i\lfloor5/i\rfloor = 1\cdot5+2\cdot2+3\cdot1+4\cdot1+5\cdot1 = 5+4+3+4+5=21\)\(50-21=29\) ✓(手算 \(0+1+2+1+0+5+5+5+5+5=29\))。

四个坑

  1. \(n\) 可能大于 \(k\)。此时 \(i\in(k,n]\) 的每项都等于 \(k\), 这部分被 \(nk\) 和「\(\sum\) 只算到 \(\min(n,k)\)」自动吸收,不要重复加
  2. 右端点必须对 \(m\) 取 min,否则 \(i\) 会越界到 \(n\) 以外(且 \(l>k\)k//l == 0 会除零);
  3. 等差数列求和用整数 // 2,不要用 / 2(Python 3 的 / 是浮点, \(5\times10^{17}\) 量级会丢精度,见 23-浮点与科学计数法);
  4. 答案量级最大约 \(nk/2 \approx 5\times10^{17}\),C++ 必须 long long

规模\(O(\sqrt k)\approx6.3\times10^4\) 次循环,毫秒级。

题解见 solutions/BISHI42.py

BISHI39 【模板】Pollard-Rho算法(中等)

\(T \le 10^5\) 组,每组给 \(1 \le x \le 10^{12}\),判断是否为质数,输出 Yes / No。 题面见 BISHI39 原题(牛客)

题目名字是个陷阱:它挂着 Pollard-Rho 的名字,但问的只是素性判定, 根本不需要把 \(x\) 分解出来。核心是 Miller-Rabin。 (Pollard-Rho 是「找非平凡因子」的算法,它内部还要靠 Miller-Rabin 判断递归边界。)

\(x \le 10^{12} < 3.47\times10^{12}\),所以底数集合 \(\{2,3,5,7,11,13\}\)确定性的。

import sys

SMALL = (2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47,
         53, 59, 61, 67, 71, 73, 79, 83, 89, 97)
# 对 n < 3.47e12 而言,这组底数的 Miller-Rabin 是确定性的
BASES = (2, 3, 5, 7, 11, 13)


def is_prime(n):
    if n < 2:
        return False                   # 1 和 0 都不是质数,且会让下面的逻辑失效
    # 100 以内的小素数试除:绝大多数合数在这里就出局,主循环只留给大素数与大半素数。
    # 顺带保证了 n > 97,底数 a 不会等于 n 本身(否则 a mod n = 0,判定失真)
    for p in SMALL:
        if n % p == 0:
            return n == p              # 小素数本身算质数,其余倍数直接判负
    d = n - 1                          # 把 n-1 写成 d * 2^s,d 为奇数
    s = 0
    while d % 2 == 0:
        d //= 2
        s += 1
    for a in BASES:
        x = pow(a, d, n)               # 内置快速幂,C 实现
        if x == 1 or x == n - 1:
            continue                   # 链首已是 ±1,这个底数证伪不了
        for _ in range(s - 1):         # 还剩 s-1 次平方;最后一次得到的是 a^(n-1)
            x = x * x % n              # Python 大整数不溢出,C++ 这里要 __int128
            if x == n - 1:
                break                  # 出现 -1,符合质数应有的形态
        else:
            return False               # 没有出现 -1,判定为合数
    return True


def main():
    data = sys.stdin.buffer.read().split()
    t = int(data[0])
    cache = {}                             # T 到 1e5 时重复询问很常见,记住算过的答案
    out = []
    for i in range(1, t + 1):
        x = int(data[i])
        r = cache.get(x)
        if r is None:                      # 用 None 而不是假值判断,答案本身是字符串
            r = "Yes" if is_prime(x) else "No"
            cache[x] = r
        out.append(r)
    sys.stdout.write("\n".join(out) + "\n")   # 一次性输出,10 万行逐行 print 会被 IO 拖死


main()

两处压常数的手段\(T=10^5\) 时都必要):

  1. 先用 100 以内的小素数试除。绝大多数合数在这里就被筛掉, 只有「大素数 / 大半素数」才真正走进 Miller-Rabin 的主循环;
  2. 用字典缓存已算过的 \(x\)\(T\)\(10^5\) 时重复询问很常见。

四个坑

  1. \(x=1\) 不是质数,必须特判(\(n-1=0\) 会让 Miller-Rabin 的逻辑失效);
  2. 底数 \(a\) 可能等于 \(n\) 本身(\(n=2,3,5,7,11,13\)),此时 \(a \bmod n = 0\), 所以要先把小素数直接判 Yes 再进主流程;
  3. 输出是 Yes / No(首字母大写),不是 YES / NO
  4. 不需要随机数——确定性底数让同一输入多次运行结果必然一致。

Python 版的代码比 C++ 短一半:C++ 在 \(x\sim10^{12}\)x * x % n 就已经越过 long long\(10^{24}\)),必须写 __int128 或龟速乘; Python 直接写 x * x % n。这是本章开头说的那处「巨大优势」。

题解见 solutions/BISHI39.py

BISHI40 数组取精(中等)

两个长度 \(n \le 10^5\) 的正整数序列 \(A, B\)。求一个下标子集 \(P\)\(|P| \le \lfloor n/2\rfloor+1\),且 \(2\sum_{i\in P}a_i > \sum a_i\)\(2\sum_{i\in P}b_i > \sum b_i\) (两边都严格过半)。输出任意一组解。 题面见 BISHI40 原题(牛客)

这题其实不是数论题,大纲把它排在本章,但真正的考点是配对贪心构造。 放在这里当作「数论章里的一道换脑子的题」也无妨——它的证明手法(构造单射) 在数论推导里同样常见。

构造

  1. 把下标按 \(a\) 从大到小排序,得 \(o_1, o_2, \ldots, o_n\)
  2. 必选 \(o_1\)\(a\) 最大的那个);
  3. \(o_2..o_n\) 两两分组 \((o_2,o_3), (o_4,o_5), \ldots\),每组选 \(b\) 更大的那个;
  4. \(n\) 为偶数,\(o_n\) 落单,也一并选上。

选出的个数 \(=1+\lfloor(n-1)/2\rfloor\ (+1\text{ 若 }n\text{ 偶}) = \lfloor n/2\rfloor+1\),正好卡满上限。

为什么 \(a\) 那边一定过半? 把每个「未选中」的元素映射到「前一组里选中的元素」 (第 1 组的未选者映射到 \(o_1\))。由于按 \(a\) 降序排,前一组的任意元素的 \(a\) 都不小于后一组的任意元素,所以这是一个单射且每对都满足「选中 \(\ge\) 未选中」。 选中集合比未选中集合多至少一个元素(最后一组的选中者没有被映射到), 而 \(a_i \ge 1 > 0\),所以 \(\sum_{\text{选中}} > \sum_{\text{未选中}}\),即 $2\sum_{\text{选中}} > $ 总和。

\(b\) 那边更直接:每组里选的就是 \(b\) 大的那个,再加上白送的 \(o_1\)\(b\ge1>0\)), 同样严格过半。\(\square\)

import sys


def main():
    data = sys.stdin.buffer.read().split()
    n = int(data[0])
    a = [int(x) for x in data[1:n + 1]]              # 前 n 个是 A,紧接着 n 个是 B
    b = [int(x) for x in data[n + 1:2 * n + 1]]

    # 排序键取负号实现降序,排的是下标而不是值——最后要输出的是下标
    order = sorted(range(n), key=lambda i: -a[i])   # 按 a 降序
    pick = [order[0]]                                # a 最大的必选
    i = 1
    # 从第二名起两两一组:组内 a 的差距已被降序压住,于是可以放心按 b 取舍
    while i + 1 < n:                                 # 成对处理,组内取 b 大者
        x, y = order[i], order[i + 1]
        pick.append(x if b[x] >= b[y] else y)
        i += 2
    if i < n:                                        # n 为偶数时最后一个落单
        pick.append(order[i])                        # 落单的也收下,仍不超过上限

    # 输出下标是 1-based,所以统一 +1
    sys.stdout.write("%d\n%s\n" % (len(pick),
                                   " ".join(str(p + 1) for p in pick)))


main()

五个坑

  1. 严格过半(\(2\Sigma >\) 总和),「恰好一半」不算;
  2. \(n=1\) 时答案就是 \(\{1\}\)\(\lfloor1/2\rfloor+1=1\)),\(2a_1>a_1\) 成立;
  3. \(n=2\) 时两个都要选,此时 \(2\Sigma = 2\times\)总和 \(>\) 总和;
  4. 排序键必须是 \(a\),组内取舍必须看 \(b\),反过来就证不出来;
  5. 输出下标是 1-based,且答案不唯一,本地必须 special judge (本仓库的 solutions/_spj/BISHI40.py 就是干这个的)。

题解见 solutions/BISHI40.py


83.8 本章速查

公式

名称 内容
整除分块取值数 \(\lfloor n/i\rfloor\) 只有 \(O(\sqrt n)\) 种取值
块右端点 \(v=\lfloor n/l\rfloor\) 时最大的 \(r = \lfloor n/v\rfloor\)
格点容斥 \(\sum_{i\le n}\lfloor n/i\rfloor = 2\sum_{i\le s}\lfloor n/i\rfloor - s^2\)\(s=\lfloor\sqrt n\rfloor\)
取模拆分 \(k\bmod i = k - i\lfloor k/i\rfloor\)
约数个数前缀和 \(\sum_{i\le n} d(i) = \sum_{i\le n}\lfloor n/i\rfloor\)
约数和前缀和 \(\sum_{i\le n}\sigma(i) = \sum_{i\le n} i\lfloor n/i\rfloor\)
二次探测 \(p\) 质数、\(x^2\equiv1 \Rightarrow x\equiv\pm1 \pmod p\)
Miller-Rabin \(n-1=d2^s\),检查 \(a^d\equiv1\)\(\exists r,\ a^{d2^r}\equiv-1\)
莫比乌斯 \(\sum_{d\mid n}\mu(d)=[n=1]\)
莫比乌斯反演 \(F=f*\mathbf1 \iff f=F*\mu\)
\(\gcd=1\) 计数 \(\sum_{d}\mu(d)\lfloor N/d\rfloor\lfloor M/d\rfloor\)
\(\gcd\) 求和 \(\sum_{d}\varphi(d)\lfloor N/d\rfloor\lfloor M/d\rfloor\)
卷积恒等式 \(\mu*\mathbf1=\varepsilon\)\(\varphi*\mathbf1=\operatorname{id}\)\(\mathbf1*\mathbf1=d\)\(\varphi=\mu*\operatorname{id}\)

确定性 Miller-Rabin 底数

\(n <\) 底数
\(3.2\times10^9\) 2, 3, 5, 7
\(3.47\times10^{12}\) 2, 3, 5, 7, 11, 13
\(3.3\times10^{24}\) 前 13 个质数

Python 取舍

场景 做法
\(\sum\lfloor n/i\rfloor\) 格点容斥 + sum(生成器),循环下沉到 C
一般整除分块 while l <= m: v = n//l; r = min(m, n//v); ...
上界 \(m>n\) m = min(n, m),否则 n//l == 0 除零
平方根 math.isqrt,永远
等差求和 (l+r)*(r-l+1)//2,整数除法
大数模乘 直接 a*b%n,不需要龟速乘 / __int128
素性判定 pow(a,d,n) + 确定性底数,不用随机
\(n\le10^{12}\) 单次分解 试除法就够,别急着上 Pollard-Rho
线性筛 \(\mu\)/\(\varphi\) \(n \le 2\times10^6\)(纯 Python 双重循环)

看到什么 → 想到什么

题面特征 第一反应
式子里有 \(\lfloor n/i\rfloor\)\(n\bmod i\) 整除分块(先把 mod 拆成 \(k-i\lfloor k/i\rfloor\)
\(\sum\sum[\gcd(a,b)=1]\) 莫比乌斯:\(\sum\mu(d)\lfloor N/d\rfloor\lfloor M/d\rfloor\)
\(\sum\sum\gcd(a,b)\) 欧拉:\(\sum\varphi(d)\lfloor N/d\rfloor\lfloor M/d\rfloor\)
\(T\) 大 + \(n\) 大的素性判定 Miller-Rabin,确定性底数
\(n\ge10^{14}\) 要分解 Pollard-Rho
\(\sum_{d\mid n}\) 型恒等式」推导 狄利克雷卷积语言
「选一半多一点使两个和都过半」 排序 + 配对贪心(BISHI40)