跳转至

第十四章 数论与数学

数论与数学是 ACM 竞赛中出题频率最高的专题之一。从质数筛法、GCD/LCM 到组合数学、博弈论,掌握这些基础知识和算法是解决大量竞赛题目的前提。本章系统梳理核心知识点,配合推导与代码实现,帮助选手建立完整的数论知识体系。


14.1 质数与筛法

14.1.1 质数判定

定义:大于 1 的正整数,除了 1 和它本身外没有其他因数的数称为质数(素数)。

试除法判断质数:对于 \(n\),只需检查 \(2 \sim \lfloor\sqrt{n}\rfloor\) 范围内是否有 \(n\) 的因子。

\[ \text{时间复杂度: } O(\sqrt{n}) \]
#include <iostream>
using namespace std;

// 判断 n 是否为质数
// 时间复杂度: O(sqrt(n))
bool isPrime(long long n) {
    if (n < 2) return false;
    // 只需检查到 sqrt(n)
    for (long long i = 2; i * i <= n; i++) {
        if (n % i == 0) return false;
    }
    return true;
}

int main() {
    long long n;
    cin >> n;
    cout << (isPrime(n) ? "质数" : "非质数") << endl;
    return 0;
}

14.1.2 埃拉托斯特尼筛法(埃氏筛)

基本思想:从 2 开始,将每个质数的所有倍数标记为合数。

graph TD
    subgraph 埃氏筛过程 - 求 2~20 的质数
        A["初始化: 2~20 全部标记为质数"] --> B["p=2: 标记 4,6,8,10,12,14,16,18,20 为合数"]
        B --> C["p=3: 标记 9,15 为合数(6,12,18已被标记)"]
        C --> D["p=5: 无新标记(25>20)"]
        D --> E["剩余质数: 2, 3, 5, 7, 11, 13, 17, 19"]
    end
\[ \text{时间复杂度: } O(n \log \log n) \]
#include <iostream>
#include <cstring>
using namespace std;

const int MAXN = 10000010;

bool isComp[MAXN];  // isComp[i] = true 表示 i 是合数
int primes[MAXN];   // 存储找到的质数
int pcnt;           // 质数计数

// 埃拉托斯特尼筛法
// 求 [2, n] 范围内的所有质数
// 时间复杂度: O(n log log n)
void sieveEratosthenes(int n) {
    memset(isComp, 0, sizeof(isComp));
    pcnt = 0;
    for (int i = 2; i <= n; i++) {
        if (!isComp[i]) {
            primes[pcnt++] = i;  // i 是质数
            // 从 i*i 开始标记,因为 i*2, i*3, ..., i*(i-1)
            // 已经被更小的质数标记过了
            for (long long j = (long long)i * i; j <= n; j += i) {
                isComp[j] = true;
            }
        }
    }
}

int main() {
    int n;
    cin >> n;
    sieveEratosthenes(n);
    cout << n << " 以内共有 " << pcnt << " 个质数" << endl;
    return 0;
}

埃氏筛的优化

  • 内层循环从 \(i^2\) 而非 \(2i\) 开始,因为 \(i \cdot 2, i \cdot 3, \ldots, i \cdot (i-1)\) 已经被更小的质数筛过
  • 可以只筛奇数(除了 2),将空间和时间减半
  • 实现简单,常数小,在 \(n \leq 10^7\) 时效率很好

14.1.3 欧拉筛(线性筛)

埃氏筛中,合数可能被其多个质因子重复标记。欧拉筛确保每个合数只被其最小质因子筛掉一次,达到线性时间复杂度 \(O(n)\)

核心思想:遍历每个数 \(i\),用已知的质数 \(primes[j]\) 去筛 \(i \cdot primes[j]\)。当 \(i \bmod primes[j] == 0\) 时停止,保证每个合数只被最小质因子筛掉。

\[ \text{时间复杂度: } O(n) \]
#include <iostream>
#include <cstring>
using namespace std;

const int MAXN = 10000010;

bool isComp[MAXN];
int primes[MAXN];
int pcnt;

// 欧拉筛(线性筛)
// 时间复杂度: O(n)
void sieveEuler(int n) {
    memset(isComp, 0, sizeof(isComp));
    pcnt = 0;
    for (int i = 2; i <= n; i++) {
        if (!isComp[i]) {
            primes[pcnt++] = i;  // i 是质数
        }
        // 用已知质数去筛 i * primes[j]
        for (int j = 0; j < pcnt && (long long)i * primes[j] <= n; j++) {
            isComp[i * primes[j]] = true;
            // 关键: i % primes[j] == 0 时停止
            // 此时 primes[j] 是 i 的最小质因子
            // 继续筛下去会导致重复标记
            if (i % primes[j] == 0) break;
        }
    }
}

int main() {
    int n;
    cin >> n;
    sieveEuler(n);
    cout << n << " 以内共有 " << pcnt << " 个质数" << endl;
    return 0;
}

为什么 \(i \bmod primes[j] == 0\) 时要停止?

\(i = k \cdot primes[j]\)。若继续用 \(primes[j+1]\)\(i \cdot primes[j+1] = k \cdot primes[j] \cdot primes[j+1]\),这个合数会在后续 \(i' = k \cdot primes[j+1]\) 时被 \(primes[j]\) 再筛一次。为了保证每个数只被筛一次,在此处停止。

筛法 时间复杂度 空间复杂度 特点
试除法(单个判断) \(O(\sqrt{n})\) \(O(1)\) 适合判断单个大数
埃氏筛 \(O(n \log \log n)\) \(O(n)\) 实现简单,常数小
欧拉筛 \(O(n)\) \(O(n)\) 线性,不重复标记

14.2 质因数分解

14.2.1 基本分解

算术基本定理:每个大于 1 的正整数都能唯一地表示为质数的乘积(不考虑顺序):

\[ n = p_1^{a_1} \cdot p_2^{a_2} \cdot \ldots \cdot p_k^{a_k} \]

试除法分解:从小到大尝试每个可能的因子。

\[ \text{时间复杂度: } O(\sqrt{n}) \]
#include <iostream>
#include <vector>
using namespace std;

// 质因数分解
// 返回: {质因子, 指数} 的列表
// 时间复杂度: O(sqrt(n))
vector<pair<long long, int>> factorize(long long n) {
    vector<pair<long long, int>> factors;
    for (long long i = 2; i * i <= n; i++) {
        if (n % i == 0) {
            int cnt = 0;
            while (n % i == 0) {
                n /= i;
                cnt++;
            }
            factors.push_back({i, cnt});
        }
    }
    // 如果 n > 1,说明 n 本身是一个质因子
    if (n > 1) {
        factors.push_back({n, 1});
    }
    return factors;
}

int main() {
    long long n;
    cin >> n;
    auto factors = factorize(n);
    cout << n << " = ";
    for (int i = 0; i < (int)factors.size(); i++) {
        if (i > 0) cout << " * ";
        cout << factors[i].first;
        if (factors[i].second > 1) cout << "^" << factors[i].second;
    }
    cout << endl;
    return 0;
}

14.2.2 利用筛法预处理最小质因子

配合欧拉筛,可以预处理 \([1, n]\) 中每个数的最小质因子(LPF),之后任意数的分解只需 \(O(\log n)\)

#include <iostream>
#include <vector>
using namespace std;

const int MAXN = 10000010;

int lpf[MAXN];  // lpf[i] = i 的最小质因子
int primes[MAXN], pcnt;

// 预处理最小质因子
void preprocess(int n) {
    lpf[1] = 1;
    for (int i = 2; i <= n; i++) {
        if (lpf[i] == 0) {
            lpf[i] = i;
            primes[pcnt++] = i;
        }
        for (int j = 0; j < pcnt && primes[j] <= lpf[i] && (long long)i * primes[j] <= n; j++) {
            lpf[i * primes[j]] = primes[j];
        }
    }
}

// 利用 lpf 数组快速分解质因数
// 时间复杂度: O(log n)
vector<pair<int, int>> fastFactorize(int n) {
    vector<pair<int, int>> factors;
    while (n > 1) {
        int p = lpf[n], cnt = 0;
        while (n % p == 0) {
            n /= p;
            cnt++;
        }
        factors.push_back({p, cnt});
    }
    return factors;
}

int main() {
    preprocess(10000000);
    int n;
    cin >> n;
    auto factors = fastFactorize(n);
    for (auto& [p, e] : factors) {
        cout << p << "^" << e << " ";
    }
    cout << endl;
    return 0;
}

质因数分解的竞赛应用

  • 因数个数\(n = p_1^{a_1} \cdots p_k^{a_k}\) 的因数个数为 \((a_1+1)(a_2+1)\cdots(a_k+1)\)
  • 因数之和\(\sigma(n) = \prod_{i=1}^{k} \frac{p_i^{a_i+1} - 1}{p_i - 1}\)
  • 欧拉函数\(\varphi(n) = n \prod_{i=1}^{k} \left(1 - \frac{1}{p_i}\right)\)

14.3 GCD / LCM 与扩展欧几里得

14.3.1 辗转相除法(GCD)

欧几里得算法\(\gcd(a, b) = \gcd(b, a \bmod b)\),直到 \(b = 0\) 时,\(\gcd(a, 0) = a\)

\[ \text{时间复杂度: } O(\log \min(a, b)) \]
// 辗转相除法求最大公约数
// 时间复杂度: O(log min(a, b))
long long gcd(long long a, long long b) {
    return b == 0 ? a : gcd(b, a % b);
}

// 最小公倍数
long long lcm(long long a, long long b) {
    return a / gcd(a, b) * b;  // 先除后乘防溢出
}

14.3.2 扩展欧几里得算法

问题:求整数 \(x, y\) 使得 \(ax + by = \gcd(a, b)\)

原理推导

\(b = 0\) 时,\(\gcd(a, 0) = a\),此时 \(x = 1, y = 0\)

\(b \neq 0\) 时,设已求出 \(bx' + (a \bmod b)y' = \gcd(b, a \bmod b)\)

由于 \(a \bmod b = a - \lfloor a/b \rfloor \cdot b\),代入得:

\[ bx' + \left(a - \lfloor a/b \rfloor \cdot b\right)y' = \gcd(a, b) \]
\[ ay' + b\left(x' - \lfloor a/b \rfloor \cdot y'\right) = \gcd(a, b) \]

因此:\(x = y'\)\(y = x' - \lfloor a/b \rfloor \cdot y'\)

graph TD
    subgraph 扩展欧几里得递推
        A["ax + by = gcd(a, b)"] --> B{"b == 0 ?"}
        B -->|是| C["gcd = a, x = 1, y = 0"]
        B -->|否| D["递归求 bx' + (a%b)y' = gcd"]
        D --> E["x = y', y = x' - (a/b)*y'"]
    end
#include <iostream>
using namespace std;

// 扩展欧几里得算法
// 求 ax + by = gcd(a, b) 的一组整数解
// 返回值: gcd(a, b)
// 时间复杂度: O(log min(a, b))
long long exgcd(long long a, long long b, long long& x, long long& y) {
    if (b == 0) {
        x = 1;
        y = 0;
        return a;
    }
    long long g = exgcd(b, a % b, y, x);
    // 注意: 这里 y 和 x 交换了
    // 实际上 x = y', y = x' - (a/b) * y'
    y -= a / b * x;
    return g;
}

int main() {
    long long a, b, x, y;
    cin >> a >> b;
    long long g = exgcd(a, b, x, y);
    cout << "gcd = " << g << endl;
    cout << "x = " << x << ", y = " << y << endl;
    cout << a << " * " << x << " + " << b << " * " << y << " = " << g << endl;
    return 0;
}

14.3.3 解线性同余方程

问题:求 \(ax \equiv b \pmod{m}\) 的解。

转化为:\(ax + my = b\),即求解不定方程。当且仅当 \(\gcd(a, m) \mid b\) 时有解。

// 解 ax ≡ b (mod m)
// 返回: 最小非负整数解,无解返回 -1
long long solveModEq(long long a, long long b, long long m) {
    long long x, y;
    long long g = exgcd(a, m, x, y);
    if (b % g != 0) return -1;  // 无解
    // 调整解到 [0, m/g) 范围内
    x = x * (b / g) % (m / g);
    if (x < 0) x += m / g;
    return x;
}

扩展欧几里得的重要性

扩展欧几里得算法是数论中的核心工具。它不仅用于求 GCD,还是求解模逆元、中国剩余定理等高级算法的基础。竞赛中经常需要灵活运用它来解决各种模方程问题。


14.4 快速幂与矩阵快速幂

14.4.1 快速幂

问题:计算 \(a^b \bmod m\)

朴素方法需要 \(O(b)\) 次乘法。快速幂利用二进制分解将复杂度降至 \(O(\log b)\)

原理:将指数 \(b\) 写成二进制形式 \(b = (c_k c_{k-1} \ldots c_1 c_0)_2\),则:

\[ a^b = a^{c_k \cdot 2^k} \cdot a^{c_{k-1} \cdot 2^{k-1}} \cdot \ldots \cdot a^{c_0 \cdot 2^0} \]
graph LR
    subgraph 快速幂示例: 计算 3^13
        A["13 = 1101 二进制"] --> B["3^13 = 3^8 * 3^4 * 3^1"]
        B --> C["3^1 = 3"]
        C --> D["3^2 = 9"]
        D --> E["3^4 = 81"]
        E --> F["3^8 = 6561"]
        F --> G["结果 = 6561 * 81 * 3 = 1594323"]
    end
\[ \text{时间复杂度: } O(\log b) \]
#include <iostream>
using namespace std;

// 快速幂: 计算 a^b mod m
// 时间复杂度: O(log b)
long long qpow(long long a, long long b, long long m) {
    long long res = 1 % m;
    a %= m;
    while (b > 0) {
        // 如果 b 的当前最低位为 1,则乘上当前的 a
        if (b & 1) {
            res = res * a % m;
        }
        // a 自乘,对应指数的下一位
        a = a * a % m;
        // b 右移一位
        b >>= 1;
    }
    return res;
}

int main() {
    long long a, b, m;
    cin >> a >> b >> m;
    cout << a << "^" << b << " mod " << m << " = " << qpow(a, b, m) << endl;
    return 0;
}

14.4.2 矩阵快速幂

问题:计算矩阵 \(A^b\),其中 \(A\)\(n \times n\) 矩阵。

将快速幂中的标量乘法替换为矩阵乘法即可。常用于加速线性递推。

经典应用:计算 Fibonacci 数列第 \(n\) 项。

\[ \begin{pmatrix} F_{n+1} \\ F_n \end{pmatrix} = \begin{pmatrix} 1 & 1 \\ 1 & 0 \end{pmatrix}^n \begin{pmatrix} 1 \\ 0 \end{pmatrix} \]
\[ \text{时间复杂度: } O(n^3 \log b),其中 n 为矩阵维度 \]
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;

typedef long long ll;
typedef vector<vector<ll>> Matrix;

const int MOD = 1e9 + 7;

// 矩阵乘法: C = A * B
// A: n*m 矩阵, B: m*p 矩阵
// 时间复杂度: O(n*m*p)
Matrix mul(const Matrix& A, const Matrix& B) {
    int n = A.size(), m = B.size(), p = B[0].size();
    Matrix C(n, vector<ll>(p, 0));
    for (int i = 0; i < n; i++) {
        for (int k = 0; k < m; k++) {
            if (A[i][k] == 0) continue;  // 优化常数
            for (int j = 0; j < p; j++) {
                C[i][j] = (C[i][j] + A[i][k] * B[k][j]) % MOD;
            }
        }
    }
    return C;
}

// 矩阵快速幂: A^b
// 时间复杂度: O(n^3 log b)
Matrix qpow(Matrix A, ll b) {
    int n = A.size();
    Matrix res(n, vector<ll>(n, 0));
    for (int i = 0; i < n; i++) res[i][i] = 1;  // 单位矩阵
    while (b > 0) {
        if (b & 1) res = mul(res, A);
        A = mul(A, A);
        b >>= 1;
    }
    return res;
}

// 利用矩阵快速幂求 Fibonacci 数列第 n 项
ll fib(ll n) {
    if (n == 0) return 0;
    if (n == 1) return 1;
    Matrix A = {{1, 1}, {1, 0}};
    Matrix res = qpow(A, n - 1);
    return res[0][0];  // F(n)
}

int main() {
    ll n;
    cin >> n;
    cout << "F(" << n << ") = " << fib(n) << endl;
    return 0;
}

矩阵快速幂的使用场景

  • 线性递推关系(Fibonacci、Tribonacci 等)
  • 图的路径计数(\(A^k\)\((i,j)\) 元素表示 \(i\)\(j\) 恰好 \(k\) 步的路径数)
  • 带状态转移的 DP 加速
  • 关键:递推必须是线性的,才能用矩阵表示

14.5 模逆元与费马小定理

14.5.1 模逆元的定义

定义:对于 \(a\) 和模数 \(m\),若 \(\gcd(a, m) = 1\),则存在整数 \(x\) 使得 \(ax \equiv 1 \pmod{m}\)\(x\) 称为 \(a\)\(m\)逆元,记为 \(a^{-1}\)

用途:在模运算中,除法不能直接做。\(a / b \bmod m\) 需要转化为 \(a \cdot b^{-1} \bmod m\)

14.5.2 费马小定理

定理:若 \(p\) 是质数,且 \(\gcd(a, p) = 1\),则:

\[ a^{p-1} \equiv 1 \pmod{p} \]

由此可得:

\[ a^{-1} \equiv a^{p-2} \pmod{p} \]

利用快速幂计算 \(a^{p-2} \bmod p\),时间复杂度 \(O(\log p)\)

#include <iostream>
using namespace std;

const int MOD = 1e9 + 7;

// 快速幂
long long qpow(long long a, long long b, long long m) {
    long long res = 1 % m;
    a %= m;
    while (b > 0) {
        if (b & 1) res = res * a % m;
        a = a * a % m;
        b >>= 1;
    }
    return res;
}

// 利用费马小定理求模逆元(要求 MOD 为质数)
// 时间复杂度: O(log MOD)
long long invFermat(long long a, long long p = MOD) {
    return qpow(a, p - 2, p);
}

int main() {
    long long a;
    cin >> a;
    cout << a << "^(-1) mod " << MOD << " = " << invFermat(a) << endl;
    // 验证: a * a^(-1) % MOD 应该等于 1
    cout << "验证: " << a << " * " << invFermat(a) << " mod " << MOD
         << " = " << a % MOD * invFermat(a) % MOD << endl;
    return 0;
}

14.5.3 线性递推求逆元

当需要求 \(1, 2, \ldots, n\) 所有数的逆元时,逐一用快速幂效率为 \(O(n \log p)\)。使用递推公式可优化到 \(O(n)\)

递推公式(设 \(p\) 为质数):

\[ i^{-1} \equiv -\left\lfloor \frac{p}{i} \right\rfloor \cdot (p \bmod i)^{-1} \pmod{p} \]

推导:设 \(p = k \cdot i + r\),其中 \(k = \lfloor p/i \rfloor\)\(r = p \bmod i\)。则 \(k \cdot i + r \equiv 0 \pmod{p}\),两边同乘 \(i^{-1} \cdot r^{-1}\)\(k \cdot r^{-1} + i^{-1} \equiv 0\),即 \(i^{-1} \equiv -k \cdot r^{-1} \pmod{p}\)

#include <iostream>
using namespace std;

const int MAXN = 3000010;
const int MOD = 1e9 + 7;

long long inv[MAXN];

// 线性递推求 [1, n] 所有数的模逆元
// 要求 MOD 为质数
// 时间复杂度: O(n)
void preprocessInv(int n) {
    inv[1] = 1;
    for (int i = 2; i <= n; i++) {
        inv[i] = (MOD - MOD / i) * inv[MOD % i] % MOD;
    }
}

int main() {
    int n;
    cin >> n;
    preprocessInv(n);
    for (int i = 1; i <= n; i++) {
        cout << i << "^(-1) mod " << MOD << " = " << inv[i] << endl;
    }
    return 0;
}
方法 时间复杂度 适用场景
费马小定理 \(O(\log p)\) 每个 模数为质数,求单个逆元
扩展欧几里得 \(O(\log p)\) 每个 模数不必为质数
线性递推 \(O(n)\) 总计 批量求 \(1 \sim n\) 的逆元

14.6 组合数学

14.6.1 排列与组合

排列数:从 \(n\) 个不同元素中取 \(r\) 个排列:

\[ P(n, r) = \frac{n!}{(n - r)!} \]

组合数:从 \(n\) 个不同元素中取 \(r\) 个(不考虑顺序):

\[ C(n, r) = \binom{n}{r} = \frac{n!}{r!(n - r)!} \]

递推关系(杨辉三角)

\[ \binom{n}{r} = \binom{n-1}{r-1} + \binom{n-1}{r} \]

边界条件:\(\binom{n}{0} = \binom{n}{n} = 1\)

14.6.2 预处理组合数(模意义下)

利用阶乘和逆元,可以在 \(O(n)\) 预处理后 \(O(1)\) 查询组合数。

\[ \binom{n}{r} \bmod p = \frac{n!}{r!(n-r)!} \bmod p = n! \cdot (r!)^{-1} \cdot ((n-r)!)^{-1} \bmod p \]
#include <iostream>
using namespace std;

const int MAXN = 2000010;
const int MOD = 1e9 + 7;

long long fac[MAXN];  // 阶乘: fac[i] = i! mod MOD
long long invFac[MAXN];  // 阶乘的逆元: invFac[i] = (i!)^(-1) mod MOD

// 快速幂
long long qpow(long long a, long long b, long long m) {
    long long res = 1 % m;
    a %= m;
    while (b > 0) {
        if (b & 1) res = res * a % m;
        a = a * a % m;
        b >>= 1;
    }
    return res;
}

// 预处理阶乘和逆元
// 时间复杂度: O(n)
void preprocessComb(int n) {
    fac[0] = 1;
    for (int i = 1; i <= n; i++) {
        fac[i] = fac[i - 1] * i % MOD;
    }
    invFac[n] = qpow(fac[n], MOD - 2, MOD);  // 费马小定理求逆元
    for (int i = n - 1; i >= 0; i--) {
        invFac[i] = invFac[i + 1] * (i + 1) % MOD;  // 递推求逆元
    }
}

// 组合数 C(n, r) mod MOD
// 时间复杂度: O(1) 查询
long long C(int n, int r) {
    if (r < 0 || r > n) return 0;
    return fac[n] % MOD * invFac[r] % MOD * invFac[n - r] % MOD;
}

int main() {
    preprocessComb(2000000);
    int n, r;
    cin >> n >> r;
    cout << "C(" << n << ", " << r << ") = " << C(n, r) << endl;
    return 0;
}

14.6.3 Lucas 定理

\(n, r\) 很大但模数 \(p\) 较小(且为质数)时,使用 Lucas 定理

定理:设 \(p\) 为质数,将 \(n, r\) 写成 \(p\) 进制:

\[ n = n_k p^k + \ldots + n_1 p + n_0, \quad r = r_k p^k + \ldots + r_1 p + r_0 \]

则:

\[ \binom{n}{r} \equiv \prod_{i=0}^{k} \binom{n_i}{r_i} \pmod{p} \]

其中当 \(r_i > n_i\) 时,\(\binom{n_i}{r_i} = 0\)

适用条件\(p\) 为质数,\(p\) 不太大(通常 \(p \leq 10^5\)),\(n, r\) 可以很大。

#include <iostream>
using namespace std;

long long MOD;

// 快速幂
long long qpow(long long a, long long b, long long m) {
    long long res = 1 % m;
    a %= m;
    while (b > 0) {
        if (b & 1) res = res * a % m;
        a = a * a % m;
        b >>= 1;
    }
    return res;
}

// 预处理阶乘(模 p 意义下)
const int MAXP = 100010;
long long fac[MAXP];

void preprocessFac() {
    fac[0] = 1;
    for (int i = 1; i < MOD; i++) {
        fac[i] = fac[i - 1] * i % MOD;
    }
}

// 小范围组合数 C(n, r) mod p,用费马小定理
long long combSmall(long long n, long long r) {
    if (r < 0 || r > n) return 0;
    return fac[n] % MOD * qpow(fac[r], MOD - 2, MOD) % MOD
           * qpow(fac[n - r], MOD - 2, MOD) % MOD;
}

// Lucas 定理
// 求 C(n, r) mod p,p 为质数
// 时间复杂度: O(p + log_p(n) * log p)
long long lucas(long long n, long long r) {
    if (r == 0) return 1;
    return lucas(n / MOD, r / MOD) * combSmall(n % MOD, r % MOD) % MOD;
}

int main() {
    long long n, r;
    cin >> n >> r >> MOD;
    preprocessFac();
    cout << "C(" << n << ", " << r << ") mod " << MOD << " = " << lucas(n, r) << endl;
    return 0;
}

Lucas 定理的适用范围

  • 模数 \(p\) 必须是质数
  • 当模数不是质数但可以分解为质数幂 \(p^k\) 时,需要使用扩展 Lucas 定理(exLucas),实现更复杂
  • \(n, r < p\) 时,直接用预处理的阶乘 + 逆元即可,无需 Lucas

14.7 中国剩余定理(CRT)与扩展 CRT

14.7.1 中国剩余定理(CRT)

问题:求解同余方程组:

\[ \begin{cases} x \equiv a_1 \pmod{m_1} \\ x \equiv a_2 \pmod{m_2} \\ \vdots \\ x \equiv a_k \pmod{m_k} \end{cases} \]

CRT 定理:当 \(m_1, m_2, \ldots, m_k\) 两两互质时,方程组在 \(\bmod\ M\)\(M = m_1 m_2 \cdots m_k\))意义下有唯一解。

解法

  1. 计算 \(M = m_1 \cdot m_2 \cdots m_k\)
  2. 对每个 \(i\),计算 \(M_i = M / m_i\)
  3. \(M_i\)\(m_i\) 的逆元 \(t_i\)(因为 \(m_i\) 两两互质,\(\gcd(M_i, m_i) = 1\),逆元存在)
  4. 解为 \(x = \sum_{i=1}^{k} a_i \cdot M_i \cdot t_i \bmod M\)
#include <iostream>
#include <vector>
using namespace std;

// 扩展欧几里得(用于求逆元)
long long exgcd(long long a, long long b, long long& x, long long& y) {
    if (b == 0) { x = 1; y = 0; return a; }
    long long g = exgcd(b, a % b, y, x);
    y -= a / b * x;
    return g;
}

// 中国剩余定理
// a[i]: 余数, m[i]: 模数(两两互质)
// 返回: x mod M,其中 M = m[0]*m[1]*...*m[k-1]
long long crt(const vector<long long>& a, const vector<long long>& m) {
    int k = a.size();
    long long M = 1;
    for (int i = 0; i < k; i++) M *= m[i];

    long long x = 0;
    for (int i = 0; i < k; i++) {
        long long Mi = M / m[i];
        long long ti, y;
        exgcd(Mi, m[i], ti, y);  // Mi * ti ≡ 1 (mod m[i])
        ti = (ti % m[i] + m[i]) % m[i];
        x = (x + a[i] * Mi % M * ti % M) % M;
    }
    return x;
}

int main() {
    int k;
    cin >> k;
    vector<long long> a(k), m(k);
    for (int i = 0; i < k; i++) cin >> a[i] >> m[i];
    cout << "x = " << crt(a, m) << endl;
    return 0;
}

CRT 中的乘法溢出风险

上面代码中 x = (x + a[i] * Mi % M * ti % M) % M 这一步,a[i] * Mi 与后续的 * ti 都是在模 \(M\) 之前先做乘法。当 \(M\) 接近 long long 上限(约 \(9.2 \times 10^{18}\))时,两个 \([0, M)\) 内的数相乘会直接溢出,得到错误结果。只要 \(M > 3 \times 10^9\) 左右就有溢出可能,竞赛中模数乘积很容易超过这个范围。

解决方法是用 __int128 快速乘(mulmod)替换代码中的乘法:

// __int128 快速乘: 计算 a * b % m, 中间结果用 128 位整数存放, 不会溢出
long long mulmod(long long a, long long b, long long m) {
    return (__int128)a * b % m;
}

然后把累加一行改写为:

x = (x + mulmod(mulmod(a[i], Mi, M), ti, M)) % M;

__int128 在主流评测机(GCC/Clang,64 位环境)下均可用。若比赛环境不支持,可改用「龟速乘」(类似快速幂的加法版本,\(O(\log b)\))或 long double 技巧。14.7.2 的 EXCRT 中 cur_m * t 一步同样存在此风险,处理方式相同。

14.7.2 扩展中国剩余定理(EXCRT)

\(m_i\) 不一定两两互质时,不能直接使用 CRT。EXCRT 的方法是逐个合并方程

合并两个方程:将 \(x \equiv a_1 \pmod{m_1}\)\(x \equiv a_2 \pmod{m_2}\) 合并。

\(x = m_1 k + a_1\),代入第二个方程:\(m_1 k + a_1 \equiv a_2 \pmod{m_2}\),即 \(m_1 k \equiv a_2 - a_1 \pmod{m_2}\)

用扩展欧几里得解此方程,得到新的 \(a\)\(m = \text{lcm}(m_1, m_2)\)

#include <iostream>
#include <vector>
using namespace std;

long long exgcd(long long a, long long b, long long& x, long long& y) {
    if (b == 0) { x = 1; y = 0; return a; }
    long long g = exgcd(b, a % b, y, x);
    y -= a / b * x;
    return g;
}

// 扩展 CRT: 合并 x ≡ a (mod m) 和当前解 x ≡ cur_a (mod cur_m)
// 返回: 是否有解; 合并后 cur_a, cur_m 更新
bool merge(long long& cur_a, long long& cur_m, long long a, long long m) {
    long long x, y;
    long long g = exgcd(cur_m, m, x, y);
    // 方程: cur_m * x + m * y = g
    // 需要 cur_m * t ≡ (a - cur_a) (mod m)
    long long diff = a - cur_a;
    if (diff % g != 0) return false;  // 无解

    // t = x * (diff / g) mod (m / g)
    long long t = x * (diff / g) % (m / g);
    if (t < 0) t += m / g;

    // 更新: 新 a = cur_a + cur_m * t, 新 m = lcm(cur_m, m)
    cur_a = cur_a + cur_m * t;
    cur_m = cur_m / g * m;  // lcm
    cur_a = (cur_a % cur_m + cur_m) % cur_m;
    return true;
}

// 扩展中国剩余定理
// 返回: 最小非负整数解,无解返回 -1
long long excrt(const vector<long long>& a, const vector<long long>& m) {
    int k = a.size();
    long long cur_a = a[0], cur_m = m[0];
    for (int i = 1; i < k; i++) {
        if (!merge(cur_a, cur_m, a[i], m[i])) return -1;
    }
    return cur_a;
}

int main() {
    int k;
    cin >> k;
    vector<long long> a(k), m(k);
    for (int i = 0; i < k; i++) cin >> a[i] >> m[i];
    long long ans = excrt(a, m);
    if (ans == -1) cout << "无解" << endl;
    else cout << "x = " << ans << endl;
    return 0;
}
方法 模数要求 时间复杂度
CRT 两两互质 \(O(k \log M)\)
EXCRT 无特殊要求 \(O(k \log M)\)

14.8 容斥原理

14.8.1 基本原理

容斥原理是计数中的经典方法。设 \(A_1, A_2, \ldots, A_n\) 是有限集合,则:

\[ \left|\bigcup_{i=1}^{n} A_i\right| = \sum_{i} |A_i| - \sum_{i<j} |A_i \cap A_j| + \sum_{i<j<k} |A_i \cap A_j \cap A_k| - \ldots + (-1)^{n+1} |A_1 \cap A_2 \cap \ldots \cap A_n| \]

简记为:

\[ \left|\bigcup_{i=1}^{n} A_i\right| = \sum_{\emptyset \neq S \subseteq \{1,\ldots,n\}} (-1)^{|S|+1} \left|\bigcap_{i \in S} A_i\right| \]
graph TD
    subgraph 容斥原理示意 - 三个集合
        A["|A ∪ B ∪ C|"] --> B["= |A| + |B| + |C|"]
        B --> C["- |A∩B| - |A∩C| - |B∩C|"]
        C --> D["+ |A∩B∩C|"]
    end

14.8.2 经典应用:与 n 互质的数的个数

\([1, n]\) 中与 \(n\) 互质的数的个数,即欧拉函数 \(\varphi(n)\)

\(n\) 的质因子为 \(p_1, p_2, \ldots, p_k\),则利用容斥原理:

\[ \varphi(n) = n - \sum_{i} \frac{n}{p_i} + \sum_{i<j} \frac{n}{p_i p_j} - \ldots = n \prod_{i=1}^{k} \left(1 - \frac{1}{p_i}\right) \]
#include <iostream>
#include <vector>
using namespace std;

// 欧拉函数(利用质因数分解 + 容斥)
// 时间复杂度: O(sqrt(n))
long long eulerPhi(long long n) {
    long long result = n;
    for (long long i = 2; i * i <= n; i++) {
        if (n % i == 0) {
            // i 是 n 的质因子
            while (n % i == 0) n /= i;
            result = result / i * (i - 1);  // result *= (1 - 1/p)
        }
    }
    if (n > 1) result = result / n * (n - 1);
    return result;
}

int main() {
    long long n;
    cin >> n;
    cout << "phi(" << n << ") = " << eulerPhi(n) << endl;
    return 0;
}

14.8.3 容斥原理的位运算实现

当需要枚举集合的所有子集进行容斥时,可以用二进制枚举

#include <iostream>
#include <vector>
using namespace std;

// 容斥原理: 求 [1, n] 中至少能被给定质数之一整除的数的个数
// primes: 给定的质数列表
// n: 上界
// 时间复杂度: O(2^k * k),k 为质数个数
long long inclusionExclusion(const vector<int>& primes, long long n) {
    int k = primes.size();
    long long ans = 0;

    // 枚举所有非空子集(用二进制表示)
    for (int mask = 1; mask < (1 << k); mask++) {
        long long product = 1;
        int bits = 0;  // 子集大小
        for (int i = 0; i < k; i++) {
            if (mask & (1 << i)) {
                bits++;
                product *= primes[i];
            }
        }
        // 奇数个集合取交 → 加;偶数个 → 减
        if (bits & 1) {
            ans += n / product;
        } else {
            ans -= n / product;
        }
    }
    return ans;
}

int main() {
    long long n;
    int k;
    cin >> n >> k;
    vector<int> primes(k);
    for (int i = 0; i < k; i++) cin >> primes[i];
    cout << inclusionExclusion(primes, n) << endl;
    return 0;
}

容斥原理的使用技巧

  • 当集合数量 \(k\) 较小(通常 \(k \leq 20\))时,可以直接二进制枚举 \(2^k\) 个子集
  • \(k\) 较大时,需要结合其他技巧(如按质因子分组)
  • 容斥原理经常与 GCD、LCM、互质性等问题结合出题
  • 计算欧拉函数时,直接用公式 \(n \prod (1 - 1/p_i)\) 比容斥更高效

14.9 博弈论

14.9.1 博弈论基础概念

组合博弈(Combinatorial Game)满足以下条件的两人博弈:

  1. 两人轮流操作
  2. 信息完全公开(完美信息)
  3. 无随机因素
  4. 有限步内结束,且没有平局
  5. 最后无法操作的一方输(Normal Play Convention)

P-position(必败态):前一手的玩家(Previous player)获胜,即当前玩家必败。

N-position(必胜态):后一手的玩家(Next player)获胜,即当前玩家必胜。

判定规则

  • 终态(无法操作)是 P-position
  • 能到达至少一个 P-position 的状态是 N-position
  • 所有后继状态都是 N-position 的状态是 P-position

14.9.2 Nim 游戏

规则:有 \(n\) 堆石子,每堆分别有 \(a_1, a_2, \ldots, a_n\) 个。两人轮流操作,每次从某一堆中取走至少一个石子。取走最后一个石子的人获胜。

Sprague-Grundy 定理(Nim 和)

\[ \text{先手必胜} \iff a_1 \oplus a_2 \oplus \ldots \oplus a_n \neq 0 \]

其中 \(\oplus\) 表示按位异或(XOR)。

#include <iostream>
#include <vector>
using namespace std;

// Nim 游戏判定
// 返回: true 表示先手必胜,false 表示先手必败
bool nimGame(int n, const vector<int>& a) {
    int xorSum = 0;
    for (int i = 0; i < n; i++) {
        xorSum ^= a[i];
    }
    return xorSum != 0;  // 异或和不为 0 则先手必胜
}

int main() {
    int n;
    cin >> n;
    vector<int> a(n);
    for (int i = 0; i < n; i++) cin >> a[i];
    cout << (nimGame(n, a) ? "先手必胜" : "先手必败") << endl;
    return 0;
}

14.9.3 SG 函数(Sprague-Grundy 函数)

对于更一般的博弈,每个状态 \(x\) 可以转移到若干后继状态。定义 SG 函数

\[ \text{SG}(x) = \text{mex}\{\text{SG}(y) \mid y \text{ 是 } x \text{ 的后继状态}\} \]

其中 mex(Minimum EXcluded value)是"最小不在集合中的非负整数"。

\[ \text{mex}(S) = \min\{x \in \mathbb{N} \mid x \notin S\} \]

Sprague-Grundy 定理:多个独立子游戏的 SG 值等于各子游戏 SG 值的异或和。

\[ \text{SG}(G_1 + G_2 + \ldots + G_n) = \text{SG}(G_1) \oplus \text{SG}(G_2) \oplus \ldots \oplus \text{SG}(G_n) \]
#include <iostream>
#include <cstring>
#include <set>
#include <vector>
using namespace std;

const int MAXN = 10010;

int sg[MAXN];      // SG 函数值
bool vis[MAXN];    // 辅助数组,用于计算 mex
int moves[MAXN];   // 可行操作(如每次取 1,2,3 个石子)
int moveCnt;       // 可行操作的数量

// 计算 SG 函数(记忆化搜索)
// x: 当前状态(剩余石子数)
int getSG(int x) {
    if (sg[x] != -1) return sg[x];  // 已计算

    set<int> nextSG;  // 后继状态的 SG 值集合
    for (int i = 0; i < moveCnt; i++) {
        if (x >= moves[i]) {
            nextSG.insert(getSG(x - moves[i]));
        }
    }

    // 计算 mex
    int mex = 0;
    while (nextSG.count(mex)) mex++;
    return sg[x] = mex;
}

// 多堆博弈,利用 SG 定理
bool sgGame(int n, const vector<int>& piles) {
    int xorSum = 0;
    for (int i = 0; i < n; i++) {
        xorSum ^= getSG(piles[i]);
    }
    return xorSum != 0;
}

int main() {
    memset(sg, -1, sizeof(sg));
    sg[0] = 0;  // 终态 SG 值为 0

    // 示例: 每次可以取 1, 2, 或 3 个石子
    moveCnt = 3;
    moves[0] = 1; moves[1] = 2; moves[2] = 3;

    int n;
    cin >> n;
    vector<int> piles(n);
    for (int i = 0; i < n; i++) cin >> piles[i];

    cout << (sgGame(n, piles) ? "先手必胜" : "先手必败") << endl;
    return 0;
}

14.9.4 经典博弈模型

博弈类型 规则 SG 值 判定
取石子(Nim) 从任意一堆取任意个 \(\text{SG}(x) = x\) 异或和非 0 先手胜
Bash Game 从一堆取 \(1 \sim m\) \(\text{SG}(x) = x \bmod (m+1)\) \(n \bmod (m+1) \neq 0\) 先手胜
Wythoff Game 两堆,取同数量或从一堆取 SG 值与黄金分割有关 特征对 \((\lfloor k\phi \rfloor, \lfloor k\phi^2 \rfloor)\)
Fibonacci Game \(1 \sim 2k\) 个(\(k\) 为上次取的数) 与 Fibonacci 数列相关 \(n\) 非 Fibonacci 数时先手胜

博弈论的核心思路

  1. 找规律:先从小数据手算,尝试发现模式
  2. SG 函数:将复杂博弈分解为独立子游戏,计算各自的 SG 值
  3. 异或合并:多个独立子游戏的 SG 值取异或
  4. 特判终态:SG(终态) = 0
  5. 许多博弈题的难点不在于计算 SG,而在于识别博弈结构找规律

14.10 算法对比与总结

graph TD
    subgraph 数论算法选择指南
        A{"需要解决什么问题?"}
        A -->|"质数相关"| B["筛法 + 质因数分解"]
        A -->|"GCD / 线性方程"| C["欧几里得 + 扩展欧几里得"]
        A -->|"快速幂运算"| D["快速幂 / 矩阵快速幂"]
        A -->|"模运算中的除法"| E["模逆元"]
        A -->|"组合计数"| F["组合数预处理 / Lucas"]
        A -->|"同余方程组"| G["CRT / EXCRT"]
        A -->|"计数/互质"| H["容斥原理 / 欧拉函数"]
        A -->|"博弈/决策"| I["SG 函数 / Nim"]
        A -->|"巨大指数降幂"| J["欧拉定理 / 扩展欧拉定理"]
        A -->|"⌊n/i⌋ 求和"| K["数论分块"]
        A -->|"离散对数"| L["BSGS"]
    end
算法 适用场景 时间复杂度 空间复杂度
埃氏筛 / 欧拉筛 求范围内的质数 \(O(n \log \log n)\) / \(O(n)\) \(O(n)\)
质因数分解 分解整数 \(O(\sqrt{n})\) \(O(\log n)\)
GCD / EXGCD 最大公约数、线性方程 \(O(\log n)\) \(O(1)\)
快速幂 幂运算 \(O(\log b)\) \(O(1)\)
矩阵快速幂 线性递推加速 \(O(d^3 \log n)\) \(O(d^2)\)
模逆元 模意义下除法 \(O(\log p)\) / \(O(n)\) \(O(1)\) / \(O(n)\)
组合数 + Lucas 组合计数 \(O(1)\) 查询 / \(O(\log_p n)\) \(O(n)\) / \(O(p)\)
CRT / EXCRT 同余方程组 \(O(k \log M)\) \(O(1)\)
容斥原理 集合计数 \(O(2^k)\) \(O(k)\)
SG 函数 博弈论 \(O(n \cdot m)\) \(O(n)\)
欧拉定理 / 扩展欧拉定理 巨大指数降幂 \(O(\sqrt{m} + \log b)\) \(O(1)\)
线性筛求 \(\varphi\) 批量求欧拉函数 \(O(n)\) \(O(n)\)
数论分块 \(\sum \lfloor n/i \rfloor\) 类求和 \(O(\sqrt{n})\) \(O(1)\)
BSGS 离散对数 \(a^x \equiv b \pmod{p}\) \(O(\sqrt{p})\) \(O(\sqrt{p})\)

拓展方向

表中后四行的内容见本章 14.11 ~ 14.14 的进阶小节。此外,高斯消元可在 \(O(n^3)\) 内求解线性方程组与异或方程组(开关问题、线性基相关),属于本章的拓展内容,此处仅点到为止,遇到相关题目可查阅线性代数专题资料。


14.11 欧拉定理与扩展欧拉定理

14.11.1 从欧拉函数说起

欧拉函数 \(\varphi(n)\)\([1, n]\) 中与 \(n\) 互质的数的个数(定义与容斥推导见 14.8.2)。由质因数分解 \(n = p_1^{a_1} p_2^{a_2} \cdots p_k^{a_k}\) 可得:

\[ \varphi(n) = n \prod_{i=1}^{k}\left(1 - \frac{1}{p_i}\right) \]

单点求值直接使用 14.8.2 中的 eulerPhi 函数(\(O(\sqrt{n})\) 试除实现);需要批量求值时用 14.12 的线性筛。

14.11.2 欧拉定理

定理:若 \(\gcd(a, m) = 1\),则

\[ a^{\varphi(m)} \equiv 1 \pmod{m} \]

\(m\) 为质数 \(p\)\(\varphi(p) = p - 1\),欧拉定理退化为 14.5.2 的费马小定理,因此它是费马小定理在任意模数上的推广。

降幂推论\(\gcd(a, m) = 1\) 时,指数可以直接对 \(\varphi(m)\) 取模:

\[ a^b \equiv a^{b \bmod \varphi(m)} \pmod{m} \]

14.11.3 扩展欧拉定理(任意底数的降幂)

\(\gcd(a, m) \neq 1\) 时上面的推论不再成立。例如 \(2^4 = 16 \equiv 0 \pmod{8}\),但 \(2^{4 \bmod \varphi(8)} = 2^0 = 1\)

扩展欧拉定理:对任意整数 \(a\),只要 \(b \geq \log_2 m\),就有

\[ a^b \equiv a^{(b \bmod \varphi(m)) + \varphi(m)} \pmod{m} \]

实现时通常使用更方便的判据「\(b \geq \varphi(m)\)」(该条件下结论同样成立),即分段处理:

\[ a^b \equiv \begin{cases} a^{\,b} \pmod{m}, & b < \varphi(m) \\ a^{\,(b \bmod \varphi(m)) + \varphi(m)} \pmod{m}, & b \geq \varphi(m) \end{cases} \]

典型套路——巨大指数:求 \(2^{10^{10^5}} \bmod p\) 这类题时,指数本身有十万位,任何整数类型都存不下。做法是把 \(b\) 当作十进制字符串逐位读入,一边读一边对 \(\varphi(m)\) 取模,并用一个布尔量记录「\(b\) 是否曾经 \(\geq \varphi(m)\)」,最后按上式决定是否补加 \(\varphi(m)\),再用快速幂收尾。

\[ \text{时间复杂度: } O(\sqrt{m} + |b| + \log m),其中 |b| 为指数的十进制位数 \]

例题:洛谷 P5091 【模板】扩展欧拉定理(https://www.luogu.com.cn/problem/P5091)

题意:给定 \(a, m, b\),求 \(a^b \bmod m\)。其中 \(1 \leq a \leq 10^9\)\(1 \leq m \leq 10^8\)\(1 \leq b \leq 10^{2 \times 10^7}\)\(b\) 以十进制字符串形式给出)。

#include <iostream>
#include <string>
using namespace std;

typedef long long ll;

// O(sqrt(n)) 求欧拉函数(同 14.8.2 的 eulerPhi)
ll eulerPhi(ll n) {
    ll result = n;
    for (ll i = 2; i * i <= n; i++) {
        if (n % i == 0) {
            while (n % i == 0) n /= i;
            result = result / i * (i - 1);
        }
    }
    if (n > 1) result = result / n * (n - 1);
    return result;
}

// 快速幂
ll qpow(ll a, ll b, ll m) {
    ll res = 1 % m;
    a %= m;
    while (b > 0) {
        if (b & 1) res = res * a % m;
        a = a * a % m;
        b >>= 1;
    }
    return res;
}

int main() {
    ll a, m;
    string b;  // 指数 b 可能有几万位,以字符串读入
    cin >> a >> m >> b;

    ll phi = eulerPhi(m);
    ll r = 0;
    bool big = false;  // 记录是否有 b >= phi(m)
    for (char c : b) {
        r = r * 10 + (c - '0');
        if (r >= phi) { big = true; r %= phi; }
    }
    if (big) r += phi;  // 扩展欧拉定理: b >= phi(m) 时指数用 b mod phi(m) + phi(m)

    cout << qpow(a, r, m) << endl;
    return 0;
}
验证记录(点击展开)

以上程序已编译运行验证:

  • 输入 2 7 10\(b \geq \varphi(7)=6\) 分支):输出 2,与 \(2^{10} = 1024 \equiv 2 \pmod 7\) 一致
  • 输入 3 10 2\(b < \varphi(10)=4\) 分支):输出 9
  • 输入 \(a=2\)\(m=10^9+7\)\(b=10^{100}\)(101 位大指数):输出 314344290,与 Python pow(2, 10**100, 10**9+7) 对拍一致

扩展欧拉定理常见坑

  • \(b < \varphi(m)\) 时绝不能加 \(\varphi(m)\):此时直接计算 \(a^b\),无条件补加会得到错误结果
  • 「只取模不加 \(\varphi(m)\)」仅在 \(\gcd(a, m) = 1\) 时合法;写通用模板时统一按扩展欧拉定理的分段形式处理最稳妥
  • 幂塔 \(a^{b^{c}} \bmod m\) 需要逐层递归降幂:第二层模数为 \(\varphi(m)\)、第三层为 \(\varphi(\varphi(m))\)……\(\varphi\) 迭代 \(O(\log m)\) 次后必然到 1,递归深度有限
  • \(m = 1\) 时任何数模 1 都是 0,模板中 1 % m 的写法可自然处理该边界

14.12 线性筛求欧拉函数

14.12.1 为什么能在线性筛中顺带求出 φ

单点 \(O(\sqrt{n})\)\(\varphi\),批量求 \([1, n]\) 全部取值就要 \(O(n \sqrt{n})\),太慢。注意到 \(\varphi\)积性函数\(\gcd(a,b)=1 \Rightarrow \varphi(ab) = \varphi(a)\varphi(b)\)),且 \(\varphi(p^k) = p^k - p^{k-1}\)。而 14.1.3 的欧拉筛恰好保证每个合数只被最小质因子筛一次,这正是积性函数递推需要的全部信息。

\(p = primes[j]\) 为当前用于筛的质数,\(x = i \cdot p\),递推规则共三条:

情形 递推式 依据
\(i\) 为质数 \(\varphi(i) = i - 1\) 质数与 \([1, i-1]\) 中所有数互质
\(i \bmod p \neq 0\) \(\varphi(ip) = \varphi(i)\,(p-1)\) \(\gcd(i, p) = 1\),用积性与 \(\varphi(p) = p-1\)
\(i \bmod p = 0\) \(\varphi(ip) = \varphi(i) \cdot p\) \(ip\)\(i\) 质因子集合相同,公式 \(n\prod(1-\frac{1}{p_i})\)\(\prod\) 部分不变,前面的 \(n\) 多乘一个 \(p\)

14.12.2 模板代码

\[ \text{时间复杂度: } O(n) \]
#include <iostream>
using namespace std;

const int MAXN = 10000010;

bool isComp[MAXN];
int primes[MAXN], pcnt;
int phi[MAXN];  // phi[i] = 欧拉函数值

// 线性筛同时求欧拉函数
// 时间复杂度: O(n)
void sievePhi(int n) {
    phi[1] = 1;
    for (int i = 2; i <= n; i++) {
        if (!isComp[i]) {
            primes[pcnt++] = i;
            phi[i] = i - 1;  // 质数 p: phi(p) = p - 1
        }
        for (int j = 0; j < pcnt && (long long)i * primes[j] <= n; j++) {
            int x = i * primes[j];
            isComp[x] = true;
            if (i % primes[j] == 0) {
                // primes[j] 是 i 的最小质因子: x 与 i 质因子集合相同
                // 由公式 phi(n) = n * prod(1 - 1/p) 得 phi(x) = phi(i) * p
                phi[x] = phi[i] * primes[j];
                break;
            } else {
                // primes[j] 与 i 互质: 由积性 phi(x) = phi(i) * phi(p)
                phi[x] = phi[i] * (primes[j] - 1);
            }
        }
    }
}

int main() {
    int n;
    cin >> n;
    sievePhi(n);
    for (int i = 1; i <= n; i++) {
        cout << "phi(" << i << ") = " << phi[i] << endl;
    }
    return 0;
}

14.12.3 正确性验证(前 20 项)

上面的程序已编译运行,输出的前 20 项如下,并与 \(O(\sqrt{n})\) 单点公式在 \([1, 1000]\) 范围内逐项对拍一致:

\(n\) 1 2 3 4 5 6 7 8 9 10
\(\varphi(n)\) 1 1 2 2 4 2 6 4 6 4
\(n\) 11 12 13 14 15 16 17 18 19 20
\(\varphi(n)\) 10 4 12 6 8 8 16 6 18 8

14.12.4 例题:洛谷 P2158 [SDOI2008] 仪仗队

题目链接:https://www.luogu.com.cn/problem/P2158

题意\(N \times N\) 的方阵(坐标 \((0,0)\)\((N-1, N-1)\))站满学生,观察者位于 \((0,0)\)\((x, y)\) 处的学生可见当且仅当连线上没有其他人,即 \(\gcd(x, y) = 1\)。求可见学生总数。

分析\((1,0)\)\((0,1)\)\((1,1)\) 三个方向各可见 1 人;其余可见点关于对角线对称,只需统计 \(x > y \geq 1\)\(\gcd(x, y) = 1\) 的点数,即 \(\sum_{x=2}^{N-1} \varphi(x)\)。故答案为 \(3 + 2\sum_{x=2}^{N-1}\varphi(x)\)\(N = 1\) 时答案为 0)。

#include <iostream>
using namespace std;

const int MAXN = 40010;

bool isComp[MAXN];
int primes[MAXN], pcnt;
int phi[MAXN];

// 线性筛同时求欧拉函数
void sievePhi(int n) {
    phi[1] = 1;
    for (int i = 2; i <= n; i++) {
        if (!isComp[i]) {
            primes[pcnt++] = i;
            phi[i] = i - 1;
        }
        for (int j = 0; j < pcnt && (long long)i * primes[j] <= n; j++) {
            int x = i * primes[j];
            isComp[x] = true;
            if (i % primes[j] == 0) {
                phi[x] = phi[i] * primes[j];
                break;
            } else {
                phi[x] = phi[i] * (primes[j] - 1);
            }
        }
    }
}

int main() {
    int n;
    cin >> n;
    if (n == 1) {  // 只有自己一个点,看不到任何人
        cout << 0 << endl;
        return 0;
    }
    sievePhi(n - 1);
    long long sum = 0;
    for (int i = 2; i <= n - 1; i++) sum += phi[i];
    // 3 = (0,1)、(1,0)、(1,1) 三个方向; 其余可见点按 phi 对称计数
    cout << 3 + 2 * sum << endl;
    return 0;
}

样例输入 4 输出 9,与题面一致(\(3 + 2(\varphi(2)+\varphi(3)) = 3 + 2 \times 3 = 9\));边界 1 输出 0,均已编译验证。

线性筛求 φ 的常见坑

  • phi[1] = 1 必须手动初始化,否则涉及前缀和的题目从第一项就错
  • i % primes[j] == 0 分支中要先写 phibreak,漏掉 break 既破坏线性复杂度又会错误覆盖
  • i * primes[j]int 下可能溢出,比较时先转 long long(模板已处理)
  • \(\sum_{i=1}^{n} \varphi(i) \approx \frac{3}{\pi^2} n^2\) 增长很快,前缀和务必用 long long 存储

14.13 数论分块(整除分块)

14.13.1 问题与核心观察

问题:求 \(\sum_{i=1}^{n} \left\lfloor \dfrac{n}{i} \right\rfloor\)。朴素做法 \(O(n)\),当 \(n \sim 10^{12}\) 时无法通过。

核心观察\(\lfloor n/i \rfloor\) 至多只有 \(O(\sqrt{n})\) 个不同取值——\(i \leq \sqrt{n}\)\(i\) 本身至多 \(\sqrt{n}\) 个;\(i > \sqrt{n}\)\(\lfloor n/i \rfloor < \sqrt{n}\),取值也至多 \(\sqrt{n}\) 种。且取值相同的 \(i\) 构成连续区间,可以整块跳过。

块的右端点:设当前块左端点为 \(l\),记 \(v = \lfloor n/l \rfloor\),则满足 \(\lfloor n/r \rfloor = v\) 的最大 \(r\)

\[ r = \left\lfloor \frac{n}{v} \right\rfloor = \left\lfloor \frac{n}{\lfloor n/l \rfloor} \right\rfloor \]

\(n = 10\) 为例,分块过程如下:

\([l, r]\) \(\lfloor 10/i \rfloor\) 块内贡献 \((r-l+1) \cdot \lfloor 10/l \rfloor\)
\([1, 1]\) 10 10
\([2, 2]\) 5 5
\([3, 3]\) 3 3
\([4, 5]\) 2 4
\([6, 10]\) 1 5

总和 \(10 + 5 + 3 + 4 + 5 = 27\),只用 5 块就完成了 10 项求和。

14.13.2 模板代码

\[ \text{时间复杂度: } O(\sqrt{n}) \]
#include <iostream>
using namespace std;

typedef long long ll;

// 数论分块求 sum_{i=1}^{n} floor(n / i)
// 时间复杂度: O(sqrt(n))
ll divBlockSum(ll n) {
    ll ans = 0;
    for (ll l = 1, r; l <= n; l = r + 1) {
        r = n / (n / l);  // [l, r] 内 floor(n/i) 的值都等于 n/l
        ans += (r - l + 1) * (n / l);
    }
    return ans;
}

int main() {
    ll n;
    cin >> n;
    cout << divBlockSum(n) << endl;
    return 0;
}

14.13.3 例题:洛谷 P1403 [AHOI2005] 约数研究

题目链接:https://www.luogu.com.cn/problem/P1403

题意:设 \(f(i)\) 表示 \(i\) 的约数个数,求 \(\sum_{i=1}^{n} f(i)\)

分析:交换统计顺序——不数「每个数有几个约数」,改数「每个数 \(d\) 作为约数出现几次」:\(d\)\(d, 2d, 3d, \ldots\) 的约数,在 \([1, n]\) 内共出现 \(\lfloor n/d \rfloor\) 次。于是

\[ \sum_{i=1}^{n} f(i) = \sum_{d=1}^{n} \left\lfloor \frac{n}{d} \right\rfloor \]

直接套用上面的模板程序即可(原题 \(n \leq 10^6\)\(O(n)\) 也能过,但数论分块可以应对 \(n \leq 10^{12}\) 的加强版数据)。

已编译验证:输入 3 输出 5\(f(1)+f(2)+f(3) = 1+2+2\),与题面样例一致);输入 10 输出 27,与 14.13.1 的手算分块表吻合。

数论分块常见坑

  • 右端点公式必须写成 r = n / (n / l);循环条件 l <= n 保证了 n / l 非零,不会除零
  • 贡献 (r - l + 1) * (n / l) 两个因子都可能很大,务必用 long long;带模意义下每项先取模再累加
  • 和式带权时(如 \(\sum_i i \cdot \lfloor n/i \rfloor\)),块内权值和用等差数列求和 \(\frac{(l+r)(r-l+1)}{2}\) 计算
  • 二维形式 \(\sum_i \lfloor n/i \rfloor \lfloor m/i \rfloor\) 的右端点取 \(\min\left(\lfloor n/\lfloor n/l \rfloor\rfloor,\ \lfloor m/\lfloor m/l \rfloor\rfloor\right)\)

14.14 BSGS(大步小步算法)

14.14.1 离散对数问题

问题:给定质数 \(p\) 与整数 \(a, b\)\(\gcd(a, p) = 1\)),求最小非负整数 \(x\) 使得

\[ a^x \equiv b \pmod{p} \]

这相当于模意义下的「取对数」,称为离散对数问题。由费马小定理,\(a^x \bmod p\)\(p-1\) 为周期,因此若有解,必存在 \(x < p\),只需在 \([0, p)\) 内搜索。

大步小步(Baby-Step Giant-Step):设 \(t = \lceil \sqrt{p} \rceil\),把 \(x\) 写成 \(x = i \cdot t - j\)\(1 \leq i \leq t\)\(0 \leq j < t\)),原方程变形为

\[ \left(a^{t}\right)^{i} \equiv b \cdot a^{j} \pmod{p} \]
  • 小步:枚举 \(j \in [0, t)\),把 \(b \cdot a^j \bmod p\) 存入哈希表(键为值、值为 \(j\)
  • 大步:枚举 \(i \in [1, t]\),计算 \((a^t)^i \bmod p\) 并查表,命中即得 \(x = it - j\)
graph LR
    subgraph BSGS 求解 a^x ≡ b mod p
        A["t = ⌈√p⌉, 设 x = i·t − j"] --> B["小步: 存 b·a^j → j, 0 ≤ j < t"]
        B --> C["大步: 枚举 (a^t)^i, 1 ≤ i ≤ t"]
        C --> D{"命中哈希表?"}
        D -->|是| E["x = i·t − j"]
        D -->|否| F["无解"]
    end

14.14.2 模板代码

\[ \text{时间复杂度: } O(\sqrt{p}),哈希表插入/查询按期望 O(1) 计 \]
#include <iostream>
#include <cmath>
#include <unordered_map>
using namespace std;

typedef long long ll;

// 快速幂
ll qpow(ll a, ll b, ll m) {
    ll res = 1 % m;
    a %= m;
    while (b > 0) {
        if (b & 1) res = res * a % m;
        a = a * a % m;
        b >>= 1;
    }
    return res;
}

// BSGS: 求 a^x ≡ b (mod p) 的最小非负整数解
// 要求 p 为质数且 gcd(a, p) = 1, 无解返回 -1
// 时间复杂度: O(sqrt(p))
ll bsgs(ll a, ll b, ll p) {
    a %= p; b %= p;
    if (b == 1 % p) return 0;  // a^0 = 1
    ll t = (ll)ceil(sqrt((double)p));

    // Baby step: 存 b * a^j -> j (0 <= j < t)
    // 相同键后写入的 j 更大, 对应的解 x = i*t - j 更小
    unordered_map<ll, ll> table;
    ll cur = b;
    for (ll j = 0; j < t; j++) {
        table[cur] = j;
        cur = cur * a % p;
    }

    // Giant step: 依次检查 a^(i*t) 是否命中表中的 b * a^j
    ll at = qpow(a, t, p);  // a^t
    cur = at;
    for (ll i = 1; i <= t; i++) {
        auto it = table.find(cur);
        if (it != table.end()) return i * t - it->second;  // a^(i*t) = b * a^j
        cur = cur * at % p;
    }
    return -1;
}

int main() {
    ll p, b, n;
    cin >> p >> b >> n;  // 求最小的 l 使 b^l ≡ n (mod p)
    ll ans = bsgs(b, n, p);
    if (ans == -1) cout << "no solution" << endl;
    else cout << ans << endl;
    return 0;
}

14.14.3 例题:洛谷 P3846 [TJOI2007] 可爱的质数/【模板】BSGS

题目链接:https://www.luogu.com.cn/problem/P3846

题意:给定质数 \(p\) 与整数 \(b, n\)\(2 \leq b, n < p < 2^{31}\)),求最小的非负整数 \(l\) 使 \(b^l \equiv n \pmod{p}\);无解输出 no solution

上面的模板程序就是本题的完整题解。

验证记录(点击展开)

以上程序已编译运行验证:

  • 官方样例 5 2 3:输出 3\(2^3 = 8 \equiv 3 \pmod 5\)
  • 7 3 4:输出 4\(3^4 = 81 \equiv 4 \pmod 7\)
  • 7 2 3:输出 no solution(2 模 7 的幂只有 \(\{1, 2, 4\}\)
  • 大数据 998244353 3 20190810:输出 732811431,用 Python pow(3, 732811431, 998244353) 反验证结果为 20190810,正确

BSGS 常见坑

  • \(b = 1\) 时答案为 \(x = 0\),要在建表前特判(模板中 b == 1 % p 的写法同时兼容 \(p = 1\)
  • \(a \equiv 0 \pmod{p}\) 时不满足 \(\gcd(a, p) = 1\):若 \(b \equiv 0\)\(x = 1\),否则无解,需单独特判
  • 哈希表中相同的键要保留较大的 \(j\)(循环中直接赋值覆盖即可),这样求出的 \(x = it - j\) 才是最小解
  • unordered_map 最坏可被构造数据卡到 \(O(n)\) 单次操作,比赛中可自定义哈希函数或改用「排序 + 二分」
  • 模数不是质数、或 \(\gcd(a, p) \neq 1\) 时需要扩展 BSGS(exBSGS),本节模板不适用

14.15 ACM Day9 数论 练习题

本节为 ACM Day9 数论专题的配套练习。题目覆盖 GCD 性质、组合数学、快速幂、质因数分解、扩展欧几里得、扩展 CRT 等核心知识点,难度由易到难分级排列。建议先独立思考 15-20 分钟再看解题提示。

使用指南

每道题的知识点标注对应本章各小节,可快速回溯相关理论。解题提示仅给出思路方向,具体实现需自行完成。完成所有练习后,尝试用本章 14.10 的算法选择指南总结每道题所属的算法类别。

题目总表

题号 平台 题目 难度 链接 完成
1920C CF Partitioning the Array 1600 https://codeforces.com/contest/1920/problem/C - [ ]
1957C CF How Does the Rook Move? 1600 https://codeforces.com/contest/1957/problem/C - [ ]
1985G CF D-Function 1700 https://codeforces.com/contest/1985/problem/G - [ ]
2039D CF Shohag Loves GCD 1700 https://codeforces.com/contest/2039/problem/D - [ ]
2050F CF Maximum modulo equality 1700 https://codeforces.com/contest/2050/problem/F - [ ]
2065G CF Skibidus and Capping 1700 https://codeforces.com/contest/2065/problem/G - [ ]
1995C CF Squaring 1800 https://codeforces.com/contest/1995/problem/C - [ ]
abc240_d AT Strange Balls 1600 https://atcoder.jp/contests/abc240/tasks/abc240_d - [ ]
abc303_d AT Shift vs. CapsLock 1700 https://atcoder.jp/contests/abc303/tasks/abc303_d - [ ]
abc286_c AT Rotate and Palindrome 1800 https://atcoder.jp/contests/abc286/tasks/abc286_c - [ ]
P1082 洛谷 【模板】同余方程 1700 https://www.luogu.com.cn/problem/P1082 - [ ]
P4777 洛谷 【模板】扩展中国剩余定理 1800 https://www.luogu.com.cn/problem/P4777 - [ ]

知识点分布总览

知识点 对应章节 涉及题号
GCD 性质 14.3 第 1、5、7 题
组合数学 14.6 第 2 题
快速幂 14.4 第 3 题
栈与模拟 基础数据结构 第 4 题
质因数分解 14.2 第 6 题
DP 与分类讨论 动态规划 第 8 题
枚举与回文 字符串算法 第 9 题
容斥/博弈 14.8 / 14.9 第 10 题
扩展欧几里得 14.3 第 11 题
扩展 CRT 14.7 第 12 题

一、基础难度(建议 30 分钟以内完成)

学习建议

基础题旨在巩固课堂所学的基本算法模板。做题时注意手写代码而非复制模板,重点理解每一步的推导逻辑。完成后尝试用不同的方法重新求解,加深对算法本质的理解。这些题目对应本章中 GCD(14.3)、组合数学(14.6)、快速幂(14.4)等基础节的内容。

序号 题号 题目名/简述 知识点 难度 解题提示
1 CF 1920C Partitioning the Array:给定数组,求满足特定条件的最大划分方案 GCD 性质 ⭐⭐ 枚举划分块数 \(k\)\(k\) 需满足 \(k \mid n\)),对每个 \(k\) 检查每块中对应位置差值的 GCD 是否大于 1。由于 \(n\) 的因子数量有限,总复杂度可控。关键观察:若所有块的对应位置差值的 \(\gcd > 1\),则划分合法。
2 CF 1957C How Does the Rook Move?:棋盘上放置车,求方案数 组合数学 ⭐⭐ 每步操作相当于固定一行一列,剩下未被占的行列数递减 1 或 2(取决于车是否放在对角线上)。设剩余 \(m\) 行/列,递推 \(f(m) = f(m-1) + (m-1) \cdot f(m-2)\)。预处理阶乘和组合数辅助计算最终方案数。
3 CF 1995C Squaring:对数组元素反复开平方使其满足单调性 快速幂 ⭐⭐ 注意到 \(a^{2^k}\) 增长极快,当指数超过 30 左右时数值已远超题目范围。从前往后维护当前元素所需的最小平方次数,累加每步差值。若前缀全为 1 则后续无法增大,需特判无解。
4 AtCoder abc240_d Strange Balls:球入桶的消去模拟 栈与模拟 ⭐⭐ 使用栈维护连续相同球的段,每段记录(编号, 数量)。当某段数量恰好等于其编号时整段弹出。每次操作后输出栈中所有段的数量之和。注意边界:栈为空时直接压入新段。

二、中等难度(建议 45-60 分钟完成)

学习建议

中等题通常需要将多个知识点组合运用,或在基本算法的基础上做适当的变形。解题时先分析问题本质,识别可以套用的数学模型,再考虑边界情况和复杂度优化。这些题目综合了 14.2(质因数分解)、14.3(GCD 性质)、14.6(组合数学)以及基础 DP 的知识。

序号 题号 题目名/简述 知识点 难度 解题提示
5 CF 1985G D-Function:统计满足数字之和为 \(d\) 的倍数的区间 GCD 与前缀和 ⭐⭐⭐ 数字之和函数 \(D(x)\) 满足 \(D(kx) = D(x)\)。利用前缀和差分思想,统计满足 \(D(l) \equiv D(r) \pmod{k}\) 的区间对数,再乘上 \(l\) 的可选方案数。对每个 \(k\) 独立计算后求和。注意模运算下的取值范围。
6 CF 2039D Shohag Loves GCD:从给定集合选元素构造数组,使 \(a_{\gcd(i,j)} \neq \gcd(a_i, a_j)\) 对所有 \(i < j\) 成立且总和最大 质因数分解 ⭐⭐⭐ \(\Omega(i)\)\(i\) 的质因子个数(含重数),可用线性筛(14.2.2 的最小质因子数组)预处理。将集合降序排序,令 \(a_i\) 取集合中第 \(\Omega(i)+1\) 大的元素:若 \(i \mid j\)\(i \neq j\),则 \(\Omega(i) < \Omega(j)\),从而 \(a_i > a_j\),约束自然满足。所需层数为 \(\lfloor \log_2 n \rfloor + 1\),集合大小不足时输出 -1。
7 CF 2050F Maximum Modulo Equality:求区间内最大公共模数 GCD 性质 ⭐⭐⭐ 关键观察:若区间内所有元素模 \(m\) 同余,则 \(m\) 必须整除任意相邻元素之差。因此答案等于区间差分数组的 GCD。预处理差分数组的 ST 表或 Sparse Table,\(O(1)\) 查询区间 GCD。特判区间长度为 1 时答案为 \(10^9\)
8 AtCoder abc303_d Shift vs. CapsLock:大小写切换与移位的最优策略 DP 与分类讨论 ⭐⭐⭐ \(dp[i][0/1]\) 表示处理完第 \(i\) 个字符后 CapsLock 状态为关/开的最小代价。转移时需分类讨论:当前字符是否匹配当前大小写状态,以及是否按下 Shift 键。注意 Shift 和 CapsLock 代价的不对称性。
9 AtCoder abc286_c Rotate and Palindrome:旋转字符串后求最小回文修改代价 枚举与回文 ⭐⭐⭐ 枚举旋转偏移量 \(k = 0 \sim n-1\),对每种旋转计算使字符串变为回文的代价。代价计算方法:比较对称位置 \((i, n-1-i)\),统计不匹配数乘以替换代价 \(A\) 或交换代价 \(B\),取较小值。总时间 \(O(n^2)\)

三、进阶难度(建议 60-90 分钟完成)

学习建议

进阶题往往涉及算法的深层性质或多个算法的综合运用。如果卡住,建议回顾本章 14.3(扩展欧几里得)和 14.7(扩展 CRT)的推导过程,理解合并方程的核心思想。这类题在正式比赛中是区分选手实力的关键,务必反复练习直至熟练。

序号 题号 题目名/简述 知识点 难度 解题提示
10 CF 2065G Skibidus and Capping:与整数约束相关的容斥/博弈问题 容斥/博弈 ⭐⭐⭐⭐ 仔细分析题目中的约束条件,将其转化为集合计数或博弈判定问题。用容斥原理排除不合法的情况,或通过 SG 函数分析必胜必败态。参考本章 14.8(容斥原理)和 14.9(博弈论)的内容,注意边界条件和大数取模。
11 洛谷 P1082 【模板】同余方程:求 \(ax \equiv b \pmod{m}\) 的最小正整数解 扩展欧几里得 ⭐⭐⭐⭐ 直接套用扩展欧几里得模板求解。步骤:(1) 用 exgcd 求 \(\gcd(a, m)\);(2) 若 \(\gcd(a, m) \nmid b\) 则方程无解;(3) 否则特解 \(x_0 = x \cdot (b / g)\),通解为 \(x_0 + k \cdot (m/g)\);(4) 对 \(m/g\) 取模调整到 \([1, m/g]\) 范围内得到最小正整数解。
12 洛谷 P4777 【模板】扩展中国剩余定理:模数不互质的同余方程组 扩展 CRT ⭐⭐⭐⭐ 逐个合并方程:将 \(x \equiv a_1 \pmod{m_1}\)\(x \equiv a_2 \pmod{m_2}\) 合并。具体做法:设 \(x = m_1 t + a_1\),代入第二个方程得 \(m_1 t \equiv a_2 - a_1 \pmod{m_2}\),用 exgcd 解出 \(t\),新模数为 \(\text{lcm}(m_1, m_2)\)。注意溢出问题,乘法运算建议使用 __int128 或快速乘避免爆 long long(见 14.7.1 的 warning)。

四、练习策略

高效刷题建议

  1. 分层推进:先完成基础题建立信心,再攻克中等和进阶题。每层完成后进行总结回顾
  2. 限时训练:基础题每道 20 分钟,中等题每道 40 分钟,进阶题每道 60 分钟。超时后先看提示再思考,不要死磕
  3. 总结归纳:每做完一道题,回顾使用了哪些知识点,在笔记本上记录关键技巧和易错点
  4. 代码模板化:将 exgcd、快速幂、组合数预处理等常用代码整理为模板,但务必理解每一行的含义而非死记硬背
  5. 复盘错题:做错或超时的题目标记为"待复习",一周后重新做一遍检验掌握程度

推荐练习顺序

循序渐进路线

第一轮(基础巩固):第 1 题 → 第 3 题 → 第 4 题 → 第 2 题 这四道题分别练习 GCD 性质、快速幂、栈模拟和组合数学,覆盖本章最基础的四个知识点。建议在 2 小时内全部完成。

第二轮(综合运用):第 7 题 → 第 5 题 → 第 8 题 → 第 9 题 → 第 6 题 中等难度题目需要将基础算法与问题建模能力结合。重点体会 GCD 性质的灵活运用(第 5、7 题)和 DP 状态设计的思路(第 8 题)。

第三轮(深度突破):第 11 题 → 第 12 题 → 第 10 题 进阶题对应本章的核心难点。先练习扩展欧几里得的模板题(P1082),再挑战扩展 CRT(P4777),最后综合运用容斥与博弈解决第 10 题。

错误排查清单

症状 可能原因 对应解法
扩展欧几里得答案为负 未做取模调整 解出后对 \(m/\gcd\) 取模并补正
快速幂结果为 0 取模后 base 变为 0 检查 base 是否被模数整除
扩展 CRT 合并后溢出 乘法爆 long long 使用 __int128(a*b)%m 快速乘
组合数结果为 0 预处理范围不足 确保阶乘数组大小覆盖最大查询值
GCD 递归栈溢出 参数过大或为 0 使用 __gcd 或改为迭代写法

常见易错点

  • 溢出问题:GCD、快速幂、扩展 CRT 中的乘法运算容易溢出 long long 范围,必要时使用 __int128 或快速乘
  • 取模细节:负数取模需调整为正数,公式中先除后乘避免精度问题;注意模数是否为质数决定了能否使用费马小定理
  • 边界特判\(\gcd(a, 0) = a\)\(0^0 = 1\)(竞赛中通常如此约定)、模数为 1 等特殊情况不要遗漏
  • 时间复杂度:注意算法在最坏情况下的复杂度是否满足题目限制,必要时做常数优化或更换算法思路