区域赛算法模版第五章---数学与数论

文章目录

    • 五、数学与数论
      • [5.1 快速幂 / 逆元](#5.1 快速幂 / 逆元)
      • [5.2 组合数预处理](#5.2 组合数预处理)
      • [5.3 矩阵快速幂](#5.3 矩阵快速幂)
      • [5.4 最大公约数 / 扩展欧几里得](#5.4 最大公约数 / 扩展欧几里得)
      • [5.5 高斯消元法](#5.5 高斯消元法)
      • [5.6 线性基](#5.6 线性基)
      • [5.7 中国剩余定理(CRT)](#5.7 中国剩余定理(CRT))
        • [5.7.1 扩展中国剩余定理(EXCRT)](#5.7.1 扩展中国剩余定理(EXCRT))
      • [5.8 质数筛与素性测试](#5.8 质数筛与素性测试)
        • [5.8.1 埃式筛法(埃拉托斯特尼筛)](#5.8.1 埃式筛法(埃拉托斯特尼筛))
        • [5.8.2 欧拉筛(线性筛)](#5.8.2 欧拉筛(线性筛))
        • [5.8.3 米勒-拉宾素性测试(Miller-Rabin)](#5.8.3 米勒-拉宾素性测试(Miller-Rabin))
        • [5.8.4 三种方法对比](#5.8.4 三种方法对比)
        • [5.8.5 注意事项](#5.8.5 注意事项)
      • [5.9 裴蜀定理](#5.9 裴蜀定理)
        • [5.9.1 基本原理](#5.9.1 基本原理)
        • [5.9.2 基础模板(洛谷 P4549)](#5.9.2 基础模板(洛谷 P4549))
        • [5.9.3 应用一:修理宝塔(HDU 5512)](#5.9.3 应用一:修理宝塔(HDU 5512))
        • [5.9.4 应用二:扩展欧几里得求特解](#5.9.4 应用二:扩展欧几里得求特解)
        • [5.9.5 应用三:线性同余方程](#5.9.5 应用三:线性同余方程)
        • [5.9.6 应用四:线性丢番图方程](#5.9.6 应用四:线性丢番图方程)
        • [5.9.7 裴蜀定理的推论](#5.9.7 裴蜀定理的推论)
        • [5.9.8 时间复杂度总结](#5.9.8 时间复杂度总结)
        • [5.9.9 注意事项](#5.9.9 注意事项)
      • [5.x 卡特兰数和组合数](#5.x 卡特兰数和组合数)
        • [5.x.1 卡特兰数(Catalan Number)](#5.x.1 卡特兰数(Catalan Number))
          • [5.x.1.1 基本原理](#5.x.1.1 基本原理)
          • [5.x.1.2 四种公式](#5.x.1.2 四种公式)
          • [5.x.1.3 基础模板(模数为质数)](#5.x.1.3 基础模板(模数为质数))
          • [5.x.1.4 模数不为质数的情况(因子计数法)](#5.x.1.4 模数不为质数的情况(因子计数法))
          • [5.x.1.5 经典应用](#5.x.1.5 经典应用)
          • [5.x.1.6 圆上连线(洛谷 P1976)](#5.x.1.6 圆上连线(洛谷 P1976))
          • [5.x.1.7 卡特兰数的推导](#5.x.1.7 卡特兰数的推导)
          • [5.x.1.8 卡特兰数的生成函数](#5.x.1.8 卡特兰数的生成函数)
          • [5.x.1.9 复杂度总结](#5.x.1.9 复杂度总结)
          • [5.x.1.10 注意事项](#5.x.1.10 注意事项)
        • [5.x.2 二项式定理和二项式反演](#5.x.2 二项式定理和二项式反演)
        • [5.x.2.1 二项式定理](#5.x.2.1 二项式定理)
        • [5.x.2.2 杨辉三角与组合数](#5.x.2.2 杨辉三角与组合数)
        • [5.x.2.3 组合数的计算方法](#5.x.2.3 组合数的计算方法)
        • [5.x.2.4 二项式定理的应用](#5.x.2.4 二项式定理的应用)
        • [5.x.2.5 二项式反演](#5.x.2.5 二项式反演)
        • [5.x.2.6 二项式反演的典型应用](#5.x.2.6 二项式反演的典型应用)
        • [5.x.2.7 分割的方法数(大厂笔试题)](#5.x.2.7 分割的方法数(大厂笔试题))
        • [5.x.2.8 二项式定理与二项式反演对比](#5.x.2.8 二项式定理与二项式反演对比)
        • [5.x.2.9 复杂度总结](#5.x.2.9 复杂度总结)
        • [5.x.2.10 注意事项](#5.x.2.10 注意事项)
      • [5.x.3 容斥定理](#5.x.3 容斥定理)
        • [5.x.3.1 基本原理](#5.x.3.1 基本原理)
        • [5.x.3.2 基础模板(二进制枚举)](#5.x.3.2 基础模板(二进制枚举))
        • [5.x.3.3 经典应用一:错排问题](#5.x.3.3 经典应用一:错排问题)
        • [5.x.3.4 经典应用二:至少 / 恰好问题](#5.x.3.4 经典应用二:至少 / 恰好问题)
        • [5.x.3.5 经典应用三:球与盒子的计数](#5.x.3.5 经典应用三:球与盒子的计数)
        • [5.x.3.6 经典应用四:图上染色计数](#5.x.3.6 经典应用四:图上染色计数)
        • [5.x.3.7 经典应用五:莫比乌斯反演](#5.x.3.7 经典应用五:莫比乌斯反演)
        • [5.x.3.8 容斥原理的常见形式](#5.x.3.8 容斥原理的常见形式)
        • [5.x.3.9 复杂度总结](#5.x.3.9 复杂度总结)
        • [5.x.3.10 注意事项](#5.x.3.10 注意事项)
      • [5.x.4 技巧---同余原理](#5.x.4 技巧---同余原理)
        • [5.x.4.1 核心概念与性质](#5.x.4.1 核心概念与性质)
        • [5.x.4.2 常见应用](#5.x.4.2 常见应用)
          • [5.x.4.2.1 大数取模与快速幂](#5.x.4.2.1 大数取模与快速幂)
          • [5.x.4.2.2 前缀和优化:k 倍区间](#5.x.4.2.2 前缀和优化:k 倍区间)
          • [5.x.4.2.3 最长 k 倍区间](#5.x.4.2.3 最长 k 倍区间)
          • [5.x.4.2.4 第 N 个神奇数字(二分 + 容斥)](#5.x.4.2.4 第 N 个神奇数字(二分 + 容斥))
          • [5.x.4.2.5 环形问题:断环为链](#5.x.4.2.5 环形问题:断环为链)
          • [5.x.4.2.6 同余最短路](#5.x.4.2.6 同余最短路)
          • [5.x.4.2.7 高次同余方程](#5.x.4.2.7 高次同余方程)
          • [5.x.4.2.8 组合数取模](#5.x.4.2.8 组合数取模)
        • [5.x.4.3 注意事项](#5.x.4.3 注意事项)

五、数学与数论

5.1 快速幂 / 逆元

适用场景: 大指数取模、组合数预处理、线性同余方程

cpp 复制代码
class Solver {
public:
    // 快速幂:计算 a^b mod MOD
    long long pow_mod(long long a, long long b, long long mod) {
        long long res = 1;
        a %= mod;
        while (b) {
            if (b & 1) res = res * a % mod;
            a = a * a % mod;
            b >>= 1;
        }
        return res;
    }

    // 费马小定理求逆元(要求 MOD 为质数)
    long long inv(long long a, long long mod) {
        return pow_mod(a, mod - 2, mod);
    }
};

思路要点:

  • 二进制拆分指数,逐位平方,时间复杂度 O(log b)
  • 费马小定理:若 p 为质数且 gcd(a,p)=1,则 a^(p-1) ≡ 1 (mod p),故 a^(-1) ≡ a^(p-2) (mod p)
  • 适用条件:MOD 必须是质数,且 a 不是 MOD 的倍数

扩展欧几里得求逆元(模数可为任意值):

cpp 复制代码
class Solver {
public:
    // 扩展欧几里得:求 ax + by = gcd(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 d = exgcd(b, a % b, y, x);
        y -= a / b * x;
        return d;
    }

    // 通用求逆元:ax ≡ 1 (mod m)
    long long inv(long long a, long long m) {
        long long x, y;
        long long d = exgcd(a, m, x, y);
        if (d != 1) return -1;  // 逆元不存在
        return (x % m + m) % m;
    }
};
  • 扩展欧几里得适用于模数为任意值(不要求质数),但要求 gcd(a, m) = 1
  • 求解 ax ≡ 1 (mod m) 等价于解 ax + my = 1,x 的最小正整数解即为逆元

常见应用:

  • 除法取模:a / b mod p 转化为 a × inv(b) mod p
  • 线性同余方程:ax ≡ b (mod n) 有解当且仅当 gcd(a,n) | b,用 exgcd 求特解后可得通解 x = x₀ + k·(n/d)
  • 组合数预处理:见 5.2

5.2 组合数预处理

适用场景: 频繁查询组合数 C(n, k) mod p

cpp 复制代码
class Solver {
public:
    static constexpr int MOD = 1e9 + 7;
    static constexpr int MAXN = 1000005;
    long long fact[MAXN], inv_fact[MAXN];

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

    // 预处理阶乘和阶乘逆元
    void init_combo(int n) {
        fact[0] = 1;
        for (int i = 1; i <= n; i++)
            fact[i] = fact[i-1] * i % MOD;

        inv_fact[n] = pow_mod(fact[n], MOD - 2, MOD);
        for (int i = n - 1; i >= 0; i--)
            inv_fact[i] = inv_fact[i+1] * (i+1) % MOD;
    }

    // 查询 C(n, k) mod MOD
    long long C(int n, int k) {
        if (k < 0 || k > n) return 0;
        return fact[n] * inv_fact[k] % MOD * inv_fact[n-k] % MOD;
    }
};

思路要点:

  • C(n,k) = n! / (k! · (n-k)!),预处理 fact\[\] 和 inv_fact\[\] 后单次查询 O(1)
  • 逆元递推公式:inv_facti = inv_facti+1 * (i+1) % MOD
  • 适用条件:MOD 为质数且 MOD > n(若 MOD ≤ n,阶乘与 MOD 不互质,逆元不存在)

卢卡斯定理(n 很大,p ≤ 10⁵):

cpp 复制代码
class Solver {
public:
    // 计算 C(n, k) mod p,要求 p 为质数
    long long C_small(int n, int k, long long p) {
        if (k < 0 || k > n) return 0;
        long long res = 1;
        for (int i = 1; i <= k; i++) {
            res = res * (n - k + i) % p;
            res = res * pow_mod(i, p - 2, p) % p;
        }
        return res;
    }

    // 卢卡斯定理:C(n,k) ≡ C(n/p, k/p) · C(n%p, k%p) (mod p)
    long long lucas(long long n, long long k, long long p) {
        if (k == 0) return 1;
        return C_small(n % p, k % p, p) * lucas(n / p, k / p, p) % p;
    }
};
  • 卢卡斯定理将 n、k 表示为 p 进制,逐位计算组合数再相乘
  • 适用场景:n ≤ 10¹⁸,p 为质数且 p ≤ 10⁵

常见应用:

  • 组合数学计数问题
  • 卡特兰数 C(2n, n) / (n+1)
  • 卢卡斯定理处理大数组合数

5.3 矩阵快速幂

适用场景: 线性递推加速、图上路径计数

cpp 复制代码
class Solver {
public:
    static constexpr int MOD = 1e9 + 7;
    static constexpr int MAXN = 105;

    struct Matrix {
        long long m[MAXN][MAXN];
        int n;
        Matrix(int size) : n(size) { memset(m, 0, sizeof m); }

        Matrix operator*(const Matrix& b) const {
            Matrix res(n);
            for (int i = 0; i < n; i++)
                for (int k = 0; k < n; k++)
                    if (m[i][k])
                        for (int j = 0; j < n; j++)
                            res.m[i][j] = (res.m[i][j] + m[i][k] * b.m[k][j]) % MOD;
            return res;
        }
    };

    Matrix matrix_pow(Matrix a, long long k) {
        Matrix res(a.n);
        for (int i = 0; i < a.n; i++) res.m[i][i] = 1;
        while (k) {
            if (k & 1) res = res * a;
            a = a * a;
            k >>= 1;
        }
        return res;
    }
};

思路要点:

  • 矩阵乘法满足结合律,可用快速幂思想,时间复杂度 O(n³ log k)
  • 线性递推:构造转移矩阵,将递推式写成矩阵形式后快速幂求解
  • 图上路径计数:邻接矩阵的 k 次方,A^kij 表示从 i 到 j 走 k 步的方案数

斐波那契数列示例:

cpp 复制代码
// f(n) = f(n-1) + f(n-2)
// [f(n); f(n-1)] = [1 1; 1 0]^(n-1) * [f(1); f(0)]
Matrix fib_matrix(2);
fib_matrix.m[0][0] = 1; fib_matrix.m[0][1] = 1;
fib_matrix.m[1][0] = 1; fib_matrix.m[1][1] = 0;

Matrix res = matrix_pow(fib_matrix, n - 1);
long long fib_n = res.m[0][0] * 1 + res.m[0][1] * 0;  // f(1)=1, f(0)=0

常见应用:

  • 斐波那契、卢卡斯数列等线性递推
  • 图上走 k 步路径计数
  • DP 加速(状态转移方程写成矩阵形式)
  • 带常数项的递推:扩展矩阵维度,如 f(n) = a·f(n-1) + b·f(n-2) + c

5.4 最大公约数 / 扩展欧几里得

cpp 复制代码
class Solver {
public:
    // 欧几里得算法求最大公约数
    long long gcd(long long a, long long b) {
        while (b) { a %= b; swap(a, b); }
        return a;
    }

    // 扩展欧几里得:求 ax + by = gcd(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 d = exgcd(b, a % b, y, x);
        y -= a / b * x;
        return d;
    }
};

思路要点:

  • 欧几里得算法:gcd(a,b) = gcd(b, a mod b),时间复杂度 O(log min(a,b))
  • 扩展欧几里得:在求 gcd 的过程中同时求出 ax + by = gcd(a,b) 的一组解 (x, y)

通解形式:

{ x = x 0 + b d ⋅ t y = y 0 − a d ⋅ t t ∈ Z , d = gcd ⁡ ( a , b ) \begin{cases} x = x_0 + \frac{b}{d} \cdot t \\4pt y = y_0 - \frac{a}{d} \cdot t \end{cases} \quad t \in \mathbb{Z}, \quad d = \gcd(a,b) {x=x0+db⋅ty=y0−da⋅tt∈Z,d=gcd(a,b)

常见应用:

  • 求逆元:ax ≡ 1 (mod m) 等价于 ax + my = 1,解出 x 即可
  • 线性同余方程:ax ≡ b (mod n),有解当且仅当 gcd(a,n) | b,特解为 x₀ = (x·b/d) mod (n/d)
  • 二元一次不定方程:ax + by = c,有解当且仅当 gcd(a,b) | c,特解为 x₀·(c/d), y₀·(c/d)
  • 中国剩余定理(CRT):解同余方程组,也可用 exgcd 扩展为非互质模数

5.5 高斯消元法

5.5.1普通版高斯消元
  • 原理图:
    比如以下的矩阵:

{ x 1 + 2 x 2 − x 3 = 9 2 x 1 − x 2 + 2 x 3 = 7 x 1 − 2 x 2 + 2 x 3 = 0 \begin{cases} x_1 + 2x_2 - x_3 = 9 \\ 2x_1 - x_2 + 2x_3 = 7 \\ x_1 - 2x_2 + 2x_3 = 0 \end{cases} ⎩ ⎨ ⎧x1+2x2−x3=92x1−x2+2x3=7x1−2x2+2x3=0

=> 增广矩阵 =>

1 2 − 1 9 2 − 1 2 7 1 − 2 2 0 \begin{bmatrix} 1 & 2 & -1 & 9 \\ 2 & -1 & 2 & 7 \\ 1 & -2 & 2 & 0 \end{bmatrix} 1212−1−2−122970

(右上角标注: n × ( n + 1 ) n \times (n+1) n×(n+1), n n n 为元数)

最终结果(右侧):

x 1 = 4 , x 2 = 3 , x 3 = 1 x_1 = 4,\quad x_2 = 3,\quad x_3 = 1 x1=4,x2=3,x3=1

中间的行变换步骤(矩阵化简过程):

第一大步:

2 1 − 1 2 1 − 1 2 7 1 − 2 2 0 ⇒ 1 1 − 1 2 1 − 1 2 7 2 − 2 2 9 ⇒ 1 0 0 − 21 2 0 1 − 25 2 17 2 0 2 − 3 2 11 2 \begin{bmatrix} 2 & 1 & -1 & 2 \\ 1 & -1 & 2 & 7 \\ 1 & -2 & 2 & 0 \end{bmatrix} \Rightarrow \begin{bmatrix} 1 & 1 & -1 & 2 \\ 1 & -1 & 2 & 7 \\ 2 & -2 & 2 & 9 \end{bmatrix} \Rightarrow \begin{bmatrix} 1 & 0 & 0 & -\tfrac{21}{2} \\ 0 & 1 & -\tfrac{25}{2} & \tfrac{17}{2} \\ 0 & 2 & -\tfrac{3}{2} & \tfrac{11}{2} \end{bmatrix} 2111−1−2−122270 ⇒ 1121−1−2−122279 ⇒ 1000120−225−23−221217211

第二大步:

1 0 0 − 21 2 0 1 − 25 2 17 2 0 2 − 3 2 11 2 ⇒ 1 0 0 0 0 1 − 3 2 15 4 0 0 1 − 26 5 ⇒ 1 0 0 0 0 1 0 15 4 0 0 1 − 15 26 \begin{bmatrix} 1 & 0 & 0 & -\tfrac{21}{2} \\ 0 & 1 & -\tfrac{25}{2} & \tfrac{17}{2} \\ 0 & 2 & -\tfrac{3}{2} & \tfrac{11}{2} \end{bmatrix} \Rightarrow \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & -\tfrac{3}{2} & \tfrac{15}{4} \\ 0 & 0 & 1 & -\tfrac{26}{5} \end{bmatrix} \Rightarrow \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & \tfrac{15}{4} \\ 0 & 0 & 1 & -\tfrac{15}{26} \end{bmatrix} 1000120−225−23−221217211 ⇒ 1000100−2310415−526 ⇒ 1000100010415−2615

(注:此处手写数字可能比较潦草,部分数值根据上下文和最终结果推断,如 26 5 \tfrac{26}{5} 526 的手写看起来略像 20 9 \tfrac{20}{9} 920 但逻辑上应为 26 5 \tfrac{26}{5} 526)

第三大步:

1 0 0 0 0 1 0 15 4 0 0 1 − 15 26 ⇒ 1 0 0 0 0 1 0 15 4 0 0 1 1 ⇒ 1 0 0 4 0 1 0 3 0 0 1 1 \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & \tfrac{15}{4} \\ 0 & 0 & 1 & -\tfrac{15}{26} \end{bmatrix} \Rightarrow \begin{bmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & \tfrac{15}{4} \\ 0 & 0 & 1 & 1 \end{bmatrix} \Rightarrow \begin{bmatrix} 1 & 0 & 0 & 4 \\ 0 & 1 & 0 & 3 \\ 0 & 0 & 1 & 1 \end{bmatrix} 1000100010415−2615 ⇒ 10001000104151 ⇒ 100010001431

(注:图中空心的方框"□"表示被涂黑或书写中断的数字,通常代表高斯消元法中将该列主元化为 1 的过程,此处已按标准步骤还原为 1。)

text 复制代码
【题目描述】
有一个球形空间产生器能在 n 维空间中产生一个坚硬的球体。
你被困在这个 n 维球体中,只知道球面上 n+1 个点的坐标。
需要求出这个 n 维球体的球心坐标。

【输入】
第一行:整数 n(1 ≤ n ≤ 10)
接下来 n+1 行:每行 n 个实数,表示球面上一个点的 n 维坐标
每个实数精确到小数点后 6 位,绝对值不超过 20000

【输出】
一行,依次给出球心的 n 维坐标(n 个实数)
两个实数之间用一个空格隔开,每个实数精确到小数点后 3 位

【样例】
输入:
  2
  0.0 0.0
  -1.0 1.0
  1.0 0.0
输出:
  0.500 1.500

【提示】
球心:到球面上任意一点距离都相等的点。
距离:设 n 维空间上两点 A、B 坐标为 (a1,...,an)、(b1,...,bn),
则 AB 距离 = sqrt((a1-b1)^2 + (a2-b2)^2 + ... + (an-bn)^2)。
【题目思路】
这个题目,我们想可以设想,先从二维空间为例,如下图:
              * (a₁,b₁)
             /|\
            / | \
           /  |  \
          /   |   \
         /    |    \
(a₂,b₂) *     |     * (a₃,b₃)
         \    |    /
          \   |   /
           \  |  /
            \ | /
             \|/
              * (x,y)  ← 圆心
              
    三点到圆心距离相等: r,我们在这里很容易可以计算欧式距离:
    (a₁ − x)² + (b₁ − y)² = (a₂ − x)² + (b₂ − y)²
    (a₂ − x)² + (b₂ − y)² = (a₃ − x)² + (b₃ − y)²
    (a₁ − x)² + (b₁ − y)² = (a₃ − x)² + (b₃ − y)²
    那么我们可以化简等到:
    2(a2 - a1)x + 2(b2 - b1)y = a2^2 + b2^2 - a1^2 - b1^2
    2(a3 - a2)x + 2(b3 - b2)y = a3^2 + b3^2 - a2^2 - b2^2
    2(a4 - a3)x + 2(b4 - b3)y = a4^2 + b4^2 - a3^2 - b3^2
    a1,a2,a3,b1,b2,b3是我们已知的,所以这里就和高斯消元就有关系了,这道题就可以迎刃而解了.

基于以上的思路,代码如下:

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

class Solver {
public:
    int run() {
        int n;
        cin>>n;
        graph.assign(n+2,vector<double>(n+3));
        ans.assign(n+2,vector<double>(n+3));
        for(int i=1;i<=n+1;i++){
            for(int j=1;j<=n;j++){
                cin>>ans[i][j];
            }
        }
        for(int i=1;i<=n;i++){
            for(int j=1;j<=n;j++){
              graph[i][j]=2*(ans[i][j]-ans[i+1][j]);
              graph[i][n+1]+=ans[i][j]*ans[i][j] -ans[i+1][j]*ans[i+1][j];
            }
        }
        gauss(n);
        for(int i=1;i<=n;i++){
            cout<<fixed<<setprecision(3)<<graph[i][n+1]<<" ";
        }
        return 0;
    }

private:
    vector<vector<double>> graph;//建立系数的矩阵
    vector<vector<double>> ans;//建立二维的原数矩阵
    double sml=1e-7;
    void gauss(int n) {
        // 依次处理第 1 列到第 n 列
        for (int i = 1; i <= n; i++) {
            // 记录当前列绝对值最大的元素所在行
            int max = i;
            // 遍历所有尚未确定主元的行,寻找当前列绝对值最大的元素
            for (int j = 1; j <= n; j++) {
                // 前面的行如果已经有主元,就不再参与寻找
                if (j < i && abs(graph[j][j]) >= sml) {
                    continue;
                }
                // 更新当前列绝对值最大的元素所在行
                if (abs(graph[j][i]) > abs(graph[max][i])) {
                    max = j;
                }
            }
            // 将第 i 行和第 max 行交换,使最大元素成为当前主元
            swap(graph[i], graph[max]);
            // 如果当前主元不为 0
            if (abs(graph[i][i]) >= sml) {
                // 保存主元
                double tmp = graph[i][i];
                // 当前行除以主元,使主元变为 1
                for (int j = i; j <= n + 1; j++) {
                    graph[i][j] /= tmp;
                }
                // 利用当前行消去其他所有行的第 i 列
                for (int j = 1; j <= n; j++) {
                    // 不需要消除自己
                    if (i != j) {
                        // 计算需要减去的倍数
                        double rate = graph[j][i] / graph[i][i];
                        // 第 j 行减去第 i 行乘以 rate
                        for (int k = i; k <= n + 1; k++) {
                            graph[j][k] -= graph[i][k] * rate;
                        }
                    }
                }
            }
        }
    }
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    Solver solver;
    return solver.run();
}
5.5.2 高斯消元异或版
  • 原理解说:
text 复制代码
求解以下包含 4 个变量的异或线性方程组:
1. x1 ∧ x2 ∧ x3 = 0
2. x1 ∧ x3 ∧ x4 = 1
3. x2 ∧ x3 ∧ x4 = 1
4. x3 ∧ x4 = 0
可以计算的结果为:
可以通过代入消元法快速得出结果:
* 由方程 4 可得:x3 = x4。
* 代入方程 2:x1 ∧ x3 ∧ x3 = x1 ∧ 0 = 1,解得 x1 = 1。
* 代入方程 3:x2 ∧ x3 ∧ x3 = x2 ∧ 0 = 1,解得 x2 = 1。
* 代入方程 1:1 ∧ 1 ∧ x3 = 0,解得 x3 = 0,由于 x3 = x4,故 x4 = 0。
最终解为:
* x1 = 1
* x2 = 1
* x3 = 0
* x4 = 0
其实也可以换为高斯方程组的形式,他们的系数为:
       1   2   3   4   5
    ------------------------------------------------------------
  1 |  1   1   1   0   0
  2 |  1   0   1   1   1       
  3 |  0   1   1   1   1
  4 |  0   0   1   1   0
对于这里的系数可以对他们进行和加法一样的高斯消元,可以得到:
       1   2   3   4   5
    ------------------------------------------------------------  
  1 |  1   0   0   0   1
  2 |  0   1   0   0   1 ===>主元(及对角线)都为1,说明有唯一解,就也可以得到以上结果
  3 |  0   0   1   0   0
  4 |  0   0   0   1   0
text 复制代码
【题目描述】
N 盏灯,M 条无向边。每盏灯有开关。
按下一盏灯的开关,会翻转它本身以及所有与它有边直接相连的灯的状态。
初始所有灯是关的。求最少按几次开关,能让所有灯都打开。
数据保证有解。
【输入】
第一行:N、M
接下来 M 行:每行两个整数 u、v,表示一条边
【输出】
一个整数,最少按开关次数
【数据范围】
1 ≤ N ≤ 35,1 ≤ M ≤ 595,无重边无自环
【样例】
输入:
  5 6
  1 2
  1 3
  4 2
  3 4
  2 5
  5 3
输出:
  3
解释:按灯 1、4、5 的开关即可。
【题目思路】
首先我们根据题意可以假想出这样一个一共有n个点,m条无向边,每个点的初始状态都是0
根据题意可以操作任意一个点,操作后该点以及相邻点的状态都会改变
最终是希望所有点都变成1状态,那么可能会若干方案都可以做到
那么其中存在需要最少操作次数的方案,打印这个最少操作次数
题目保证一定能做到所有点都变成1状态,并且没有重边和自环,所以我们可以假想出这样一个图来:
        1 --------------------------------------------- 4
         \  \           / /
          \  \         / /
           \   \     /  /
            2   \   /  /
             \   \ /  /
              \   5  /
               \  | /
                \ |/
                  3  
对于以上这个图,我们1~5的原始状态均为0,可以操作任意一个点,操作后该点以及相邻点的状态都会改变,根据这句话,我们可以得到:
对于1这个点而言,他自己变会导致自己也改变2,5,4改变也会让他改变,然而1和他相邻的点同时改变就相当于没改变
所以我们可以用异或来形容这些状态的改变(由于所有点的初始状态均为 0,而最终要求所有点都变成 1,因此每个点必须恰好被改变奇数次。
对于任意一个点 i,能够影响它的操作包括:
* 操作点 i 自己;
* 操作所有与点 i 相邻的点。
由于状态改变两次会回到原状态,因此可以使用异或表示状态变化。):
x1 ⊕ x2 ⊕ x4 ⊕ x5 = 1;(点3的改变不会影响1的状态)
根据这样的规律,我们可以列出异或版的高斯消元方程:
* 点1 与 2、4、5 相连(自己也算一个,因为操作自己会改变自己)
* 点2 与 1、3相连
* 点3 与 2、4、5 相连
* 点4 与 1、3、5 相连
* 点5 与 1、3、4 相连
所以:
设 x_i 表示是否操作点 i(1 表示操作,0 表示不操作),每个点的最终状态要变成 1(初始为 0,所以需要改变奇数次)。
对于点 1,能影响它的点是:{1, 2, 4, 5},所以方程是:
x1 ⊕ x2 ⊕ x4 ⊕ x5 = 1
对于点 2,影响它的点是:{1, 2, 3},所以:
x1 ⊕ x2 ⊕ x3= 1
点 3,影响它的点是:{2, 3, 4, 5}:
x2 ⊕ x3 ⊕ x4 ⊕ x5 = 1
点 4,影响它的点是:{1, 3, 4, 5}:
x1 ⊕ x3 ⊕ x4 ⊕ x5 = 1
点 5,影响它的点是:{1, 3, 4, 5}:
x1 ⊕ x3 ⊕ x4 ⊕ x5 = 1
写成矩阵的形式就是:
[1 1 0 1 1 | 1]
[1 1 1 0 0 | 1]
[0 1 1 1 1 | 1]
[1 0 1 1 1 | 1]
[1 0 1 1 1 | 1]
根据这样的形式继续消元,就可以得到这种图形下的操作次数
而在高斯消元后:
* 如果没有自由变量,则方程组只有唯一解,直接统计所有 x_i 中 1 的个数即可;
* 如果存在自由变量,则说明存在多种操作方案。枚举自由变量(dfs)的取值,并通过回代求出对应的完整方案,统计其中操作次数最少的方案。
最终得到的最小操作次数就是答案。

所以模版代码如下:

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

class Solver {
public:
    int run() {
        // 分配空间
        graph.assign(MAXN, vector<int>(MAXN));
        op.resize(MAXN);
        // 输入灯的数量
        cin >> n;
        prepare();
        // 输入边数
        int m;
        cin >> m;
        for (int i = 1, u, v; i <= m; i++) {
            cin >> u >> v;
            // 无向边:操作u会影响v,操作v会影响u
            graph[u][v] = 1;
            graph[v][u] = 1;
        }
        // 高斯消元
        guass(n);
        // 检查是否有自由变量
        // 如果所有graph[i][i]都是1,说明有唯一解,没有自由变量
        int sign = 1;
        for (int i = 1; i <= n; i++) {
            if (graph[i][i] == 0) {
                sign = 0;
                break;
            }
        }
        if (sign == 1) {
            // 没有自由变量,唯一解
            // 答案就是所有graph[i][n+1]为1的个数(因为每个变量取1就操作一次)
            ans = 0;
            for (int i = 1; i <= n; i++) {
                if (graph[i][n + 1] == 1) {
                    ans++;
                }
            }
        } else {
            // 有自由变量,需要DFS枚举所有可能,找最少操作次数
            ans = n;  // 初始化答案为n(最大可能操作数)
            dfs(n, 0);
        }
        cout << ans << endl;
        return 0;
    }

private:
    int MAXN = 37;
    vector<vector<int>> graph;  // 增广矩阵,graph[i][j]表示第i个方程中x_j的系数
    vector<int> op;              // 存储每个变量的解(0或1)
    int n, ans;
    // 异或高斯消元,将矩阵化为行阶梯形
    void guass(int n) {
        for (int i = 1; i <= n; i++) {          // i表示当前处理到第几个主元
            // 寻找第i列的主元(值为1的行)
            for (int j = 1; j <= n; j++) {
                // 如果j < i且graph[j][j]已经作为主元,跳过已经处理过的行
                if (j < i && graph[j][j] == 1) {
                    continue;
                }
                // 找到第i列为1的行,交换到第i行
                if (graph[j][i] == 1) {
                    swap(graph[i], graph[j]);
                    break;
                }
            }
            // 如果graph[i][i]==1,说明找到了主元,用它消去其他行的第i列
            if (graph[i][i] == 1) {
                for (int j = 1; j <= n; j++) {
                    if (i != j && graph[j][i] == 1) {
                        // 第j行异或第i行,消去第j行的第i列
                        for (int k = i; k <= n + 1; k++) {
                            graph[j][k] ^= graph[i][k];
                        }
                    }
                }
            }
            // 如果graph[i][i]==0,说明第i列是自由变量,跳过
        }
    }
    // 初始化矩阵
    void prepare() {
        for (int i = 1; i <= n; i++) {
            // 清空第i行
            for (int j = 1; j <= n; j++) {
                graph[i][j] = 0;
            }
            // graph[i][i] = 1:操作自己会影响自己
            graph[i][i] = 1;
            // 方程右边为1:目标是把灯从0变成1
            graph[i][n + 1] = 1;
            op[i] = 0;
        }
    }
    // DFS枚举自由变量,从第n个变量往前搜索
    // i: 当前处理的变量编号(从n到1)
    // num: 当前已选择的操作次数
    void dfs(int i, int num) {
        // 剪枝:如果当前操作数已经大于等于已知最优解,没必要继续
        if (num >= ans) return;
        // 所有变量都处理完了,更新答案
        if (i == 0) {
            ans = num;
            return;
        }
        // 如果graph[i][i] == 0,说明第i个变量是自由变量(没有主元)
        if (graph[i][i] == 0) {
            // 自由变量可以取0或1,分别搜索
            // 情况1:取0
            op[i] = 0;
            dfs(i - 1, num);
            // 情况2:取1
            op[i] = 1;
            dfs(i - 1, num + 1);
        } else {
            // graph[i][i] == 1,说明第i个变量是非自由变量(有主元)
            // 根据方程 graph[i][i]*x_i + ... = graph[i][n+1]
            // 因为graph[i][i]=1,所以 x_i = graph[i][n+1] XOR (其他已知变量的和)
            int res = graph[i][n + 1];  // 方程右侧的值
            // 异或上所有已知变量(j > i的变量已经确定)
            for (int j = i + 1; j <= n; j++) {
                if (graph[i][j] == 1) {
                    res ^= op[j];
                }
            }
            op[i] = res;
            dfs(i - 1, num + res);
        }
    }
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    Solver solver;
    return solver.run();
}

5.6 线性基

适用场景: 异或空间的最大值 / 最小值 / 第 k 小值、判断某数能否被异或表示、异或方程组求解、区间异或最大值

cpp 复制代码
class Solver {
public:
    // 线性基模板
    // 约定:每一位对应一个二进制位,p[i] 表示最高位为 i 的基向量
    struct LinearBasis {
        static const int LOG = 62;   // long long 最多 63 位,留一点余量
        long long p[LOG + 1];        // 基向量,p[i] 的最高位为 i
        int cnt;                     // 基向量个数(即线性基的秩)
        LinearBasis() { clear(); }
        // 清空
        void clear() {
            memset(p, 0, sizeof(p));
            cnt = 0;
        }
        // 插入一个数 x,返回是否成功(x 不能被已有基表示则成功)
        bool insert(long long x) {
            for (int i = LOG; i >= 0; i--) {
                if (!((x >> i) & 1)) continue;   // x 的第 i 位为 0,跳过
                if (!p[i]) {                     // 该位还没有基
                    p[i] = x;
                    cnt++;
                    return true;
                }
                x ^= p[i];                       // 用已有基消去第 i 位
            }
            return false;                        // x 被消成 0,说明能被表示
        }
        // 查询能被异或表示出的最大值
        long long queryMax() {
            long long res = 0;
            for (int i = LOG; i >= 0; i--) {
                if ((res ^ p[i]) > res) res ^= p[i];
            }
            return res;
        }
        // 查询能被异或表示出的最小值(非零)
        long long queryMin() {
            if (cnt == 0) return 0;             // 只能表示 0
            for (int i = 0; i <= LOG; i++) {
                if (p[i]) return p[i];          // 最低位的基
            }
            return 0;
        }
        // 判断 x 能否被异或表示
        bool canRepresent(long long x) {
            for (int i = LOG; i >= 0; i--) {
                if (!((x >> i) & 1)) continue;
                if (!p[i]) return false;        // 该位没有基,无法表示
                x ^= p[i];
            }
            return true;                        // x 被消成 0
        }
        // 查询第 k 小的能被异或表示的数(k 从 1 开始)
        // 需要先调用 rebuild() 将基化为简化阶梯形
        long long queryKth(long long k) {
            if (k > (1LL << cnt)) return -1;    // 超出可表示范围
            long long res = 0;
            for (int i = 0; i < cnt; i++) {
                if ((k >> i) & 1) res ^= p[i];
            }
            return res;
        }
        // 将基化为简化阶梯形:每个基的最高位,在其他基中也为 0
        // 这样 p[0..cnt-1] 就是一组"干净"的基,方便 queryKth
        void rebuild() {
            for (int i = LOG; i >= 0; i--) {
                if (!p[i]) continue;
                for (int j = i - 1; j >= 0; j--) {
                    if ((p[i] >> j) & 1) p[i] ^= p[j];  // 消去低位
                }
            }
            // 把非零基紧凑排列到 p[0..cnt-1]
            int idx = 0;
            for (int i = 0; i <= LOG; i++) {
                if (p[i]) p[idx++] = p[i];
            }
            for (int i = idx; i <= LOG; i++) p[i] = 0;
        }
    };
};

思路要点:

  • 线性基是一组"极大线性无关"的基向量,任意原数都能用基向量异或表示
  • 插入时从高位到低位贪心消元,能消成 0 说明 x 可被已有基表示
  • 每个基向量的最高位互不相同,所以基的个数 = 异或空间的维度(秩)
  • 查询最大值:从高位到低位贪心,能变大就异或
  • 查询最小值:最低位的基(非零情况下)
  • 查询第 k 小:先 rebuild() 化为简化阶梯形,然后按 k 的二进制位对应异或
  • 每个数插入 O(LOG),查询 O(LOG),总复杂度 O(LOG)
  • 本质是异或空间的高斯消元,把向量组化为行最简形

常见变体:

  • 区间线性基 :带时间戳的线性基,查询 [l, r] 内能异或出的最大值

  • 线性基合并:把两个线性基的基向量互相插入,得到并集的线性基

  • 异或方程组:把方程组的系数矩阵插入线性基,判断解的存在性

  • 树上路径异或最大值:用前缀线性基 + LCA,或树上倍增维护

5.7 中国剩余定理(CRT)

适用场景: 求解同余方程组,模数两两互质

cpp 复制代码
class CRTSolver {
public:
    // 普通中国剩余定理:模数 m[1..n] 两两互质
    // 求解 x ≡ a[i] (mod m[i]),返回最小非负整数解
    long long CRT(int n, long long a[], long long m[]) {
        long long M = 1;
        for (int i = 1; i <= n; i++) M *= m[i];

        long long res = 0;
        for (int i = 1; i <= n; i++) {
            long long Mi = M / m[i];
            long long inv = exgcd_inv(Mi, m[i]);  // Mi 在模 m[i] 下的逆元
            res = (res + a[i] * Mi % M * inv % M) % M;
        }
        return (res % M + M) % M;
    }
};

思路要点:

  • 令 M = ∏ m i M = \prod m_i M=∏mi, M i = M / m i M_i = M / m_i Mi=M/mi
  • 对每个方程,求 M i M_i Mi 在模 m i m_i mi 下的逆元 t i t_i ti,即 M i t i ≡ 1 ( m o d m i ) M_i t_i \equiv 1 \pmod{m_i} Miti≡1(modmi)
  • 通解为 x = ∑ a i M i t i + k M x = \sum a_i M_i t_i + kM x=∑aiMiti+kM,取最小非负整数即答案
  • 前提:模数 m i m_i mi 两两互质,否则逆元可能不存在
  • 时间复杂度 O(n log M)
5.7.1 扩展中国剩余定理(EXCRT)

适用场景: 求解同余方程组,模数不一定互质

cpp 复制代码
class EXCRTSolver {
public:
    // 扩展中国剩余定理:模数不一定互质
    // 求解 x ≡ a[i] (mod m[i]),返回最小非负整数解
    // 若无解返回 -1
    long long EXCRT(int n, long long a[], long long m[]) {
        long long x = 0, M = 1;  // 当前解为 x,当前模数为 M

        for (int i = 1; i <= n; i++) {
            // 合并 x ≡ a[i] (mod m[i]) 与当前 x ≡ x (mod M)
            // 即求 x + M * t ≡ a[i] (mod m[i])
            // 化为 M * t ≡ a[i] - x (mod m[i])
            long long A = M, B = m[i], C = ((a[i] - x) % B + B) % B;
            long long g = exgcd(A, B);  // 求 gcd

            if (C % g != 0) return -1;  // 无解

            // 通解 t = t0 + k * (B / g),取最小非负
            long long t = mul_mod(C / g, exgcd_inv(A / g, B / g), B / g);

            // 更新 x 和 M
            x = x + M * t;
            M = M / g * B;      // lcm(M, m[i]) = M / g * m[i]
            x = (x % M + M) % M;
        }
        return x;
    }
};

思路要点:

  • 核心思想:逐个合并方程,每次把两个同余方程合并成一个
  • 合并过程:设当前解为 x x x,模数为 M M M,新方程为 x ′ ≡ a i ( m o d m i ) x' \equiv a_i \pmod{m_i} x′≡ai(modmi)
    • 令 x ′ = x + M t x' = x + M t x′=x+Mt,代入新方程得 M t ≡ a i − x ( m o d m i ) M t \equiv a_i - x \pmod{m_i} Mt≡ai−x(modmi)
    • 化为线性同余方程 A t ≡ C ( m o d B ) A t \equiv C \pmod{B} At≡C(modB),其中 A = M A = M A=M, B = m i B = m_i B=mi, C = a i − x C = a_i - x C=ai−x
    • 用扩展欧几里得求解,若 C % gcd ⁡ ( A , B ) ≠ 0 C \% \gcd(A, B) \ne 0 C%gcd(A,B)=0 则无解
  • 合并后新模数 M ′ = lcm ( M , m i ) = M / g ⋅ m i M' = \text{lcm}(M, m_i) = M / g \cdot m_i M′=lcm(M,mi)=M/g⋅mi
  • 注意使用快速乘 (mul_mod)防止 M * t 溢出
  • 时间复杂度 O(n log M)

与普通 CRT 对比:

特性 普通 CRT 扩展 CRT
模数要求 两两互质 任意
求解方法 构造 + 逆元 逐个合并 + exgcd
是否可能无解 不会 可能(C % g != 0)
时间复杂度 O(n log M) O(n log M)

5.8 质数筛与素性测试

适用场景: 快速筛出 1~n 内所有质数、预处理积性函数、大数素性判断

5.8.1 埃式筛法(埃拉托斯特尼筛)

核心思想: 先把 2~n 的所有数都视为质数。已知 2 是质数,就把 2 的所有倍数(4、6、8...)标记为合数;接着 3 是质数,就把 3 的所有倍数(6、9、12...)标记为合数;以此类推,最后剩下的就是质数。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

class Solver {
public:
    // 埃式筛:返回 1~n 内所有质数,同时用 isPrime 标记
    vector<int> sieve(int n) {
        vector<bool> isPrime(n + 1, true);
        isPrime[0] = isPrime[1] = false;

        // 从 2 开始枚举,筛掉质数的所有倍数
        // 只需枚举到 sqrt(n):任意合数 x ≤ n 必有因子 ≤ sqrt(n)
        for (int i = 2; i * i <= n; i++) {
            if (isPrime[i]) {
                // 从 i*i 开始筛,因为小于 i*i 的倍数已被更小的质数筛过
                for (int j = i * i; j <= n; j += i) {
                    isPrime[j] = false;
                }
            }
        }
        // 收集所有质数
        vector<int> primes;
        for (int i = 2; i <= n; i++) {
            if (isPrime[i]) primes.push_back(i);
        }
        return primes;
    }
};

思路要点:

  • 从 2 开始枚举,若 i 是质数,就把 i 的所有倍数标记为合数
  • 外层循环只需枚举到 n \sqrt{n} n :因为任意合数 x ≤ n x \le n x≤n 必有因子 ≤ n \le \sqrt{n} ≤n
  • 内层从 i * i 开始筛,避免重复标记
  • 缺点:一个数可能被重复筛多次,如 12 会被 2、3 都筛一次
  • 时间复杂度 O(n log log n),空间复杂度 O(n)
5.8.2 欧拉筛(线性筛)

核心思想: 每个合数只被它的最小质因子 筛一次。用已筛出的质数去标记后面的数:当前数 i 乘上质数 p 得到合数 i * p,一旦 p 是 i 的因子就立刻停止,避免更大质数重复标记。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

class Solver {
public:
    // 欧拉筛:返回 1~n 内所有质数,同时预处理最小质因子
    vector<int> eulerSieve(int n, vector<int>& minpf) {
        minpf.assign(n + 1, 0);          // minpf[i] = i 的最小质因子,0 表示质数
        vector<bool> isPrime(n + 1, true);
        isPrime[0] = isPrime[1] = false;
        vector<int> primes;
        for (int i = 2; i <= n; i++) {
            if (isPrime[i]) {
                primes.push_back(i);
                minpf[i] = 0;             // i 本身是质数
            }
            for (int p : primes) {
                if ((long long)i * p > n) break;
                isPrime[i * p] = false;
                minpf[i * p] = p;         // i*p 的最小质因子是 p
                // 保证每个合数只被最小质因子筛一次
                if (i % p == 0) break;
            }
        }
        return primes;
    }
};

思路要点:

  • 每个合数只会被它的最小质因子筛掉,因此每个数只被访问一次
  • 关键在 if (i % p == 0) break;:当 p 是 i 的最小质因子时停止,保证 i * p 的最小质因子就是 p
  • 时间复杂度 O(n),空间复杂度 O(n)
  • 可顺带预处理:最小质因子、欧拉函数、莫比乌斯函数等

两种筛法对比:

特性 埃式筛 欧拉筛
时间复杂度 O(n log log n) O(n)
是否重复筛 会 不会
实现难度 简单 中等
可扩展性 一般 强(可预处理积性函数)
适用场景 小数据、只判质数 大数据、需积性函数
5.8.3 米勒-拉宾素性测试(Miller-Rabin)

适用场景: 判断一个大整数(可达 10^18)是否是质数,比试除法快得多

核心思想: 基于费马小定理和二次探测定理。

  • 费马小定理:若 p p p 是质数, a p − 1 ≡ 1 ( m o d p ) a^{p-1} \equiv 1 \pmod{p} ap−1≡1(modp)
  • 二次探测定理:若 p p p 是质数, x 2 ≡ 1 ( m o d p ) x^2 \equiv 1 \pmod{p} x2≡1(modp) 的解只有 x ≡ 1 x \equiv 1 x≡1 或 x ≡ − 1 x \equiv -1 x≡−1

把 n − 1 n-1 n−1 写成 2 s ⋅ d 2^s \cdot d 2s⋅d 的形式( d d d 为奇数),然后随机选若干个底数 a a a,

检查是否满足 a d ≡ 1 ( m o d n ) a^d \equiv 1 \pmod{n} ad≡1(modn) 或存在某个 r < s r < s r<s 使 a 2 r d ≡ − 1 ( m o d n ) a^{2^r d} \equiv -1 \pmod{n} a2rd≡−1(modn)。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

class Miller {
public:
    typedef long long ll;
    typedef __int128 lll;   // 用于防止乘法溢出
    // 快速幂:计算 (a * b) % mod,防止 __int128 溢出
    ll mul_mod(ll a, ll b, ll mod) {
        return (lll)a * b % mod;
    }
    // 快速幂:计算 a^b % mod
    ll pow_mod(ll a, ll b, ll mod) {
        ll res = 1;
        a %= mod;
        while (b > 0) {
            if (b & 1) res = mul_mod(res, a, mod);
            a = mul_mod(a, a, mod);
            b >>= 1;
        }
        return res;
    }
    // 米勒-拉宾素性测试
    // 返回 true 表示 n 是质数(概率极高),false 表示 n 是合数
    bool isPrime(ll n) {
        if (n < 2) return false;
        if (n == 2 || n == 3) return true;
        if (n % 2 == 0) return false;

        // 把 n-1 写成 2^s * d 的形式
        ll d = n - 1;
        int s = 0;
        while (d % 2 == 0) {
            d /= 2;
            s++;
        }

        // 用一组固定的底数,足以覆盖 10^18 范围内的所有数
        // 对于 64 位整数,这组底数可以保证正确性
        vector<ll> bases = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37};
        for (ll a : bases) {
            if (a >= n) break;
            ll x = pow_mod(a, d, n);
            if (x == 1 || x == n - 1) continue;

            bool composite = true;
            for (int r = 1; r < s; r++) {
                x = mul_mod(x, x, n);
                if (x == n - 1) {
                    composite = false;
                    break;
                }
            }
            if (composite) return false;
        }
        return true;
    }
    // 如果需要支持 10^18 以上的范围,可以用随机底数
    // 但竞赛中固定底数 {2,3,5,7,11,13,17,19,23,29,31,37} 已经足够
};

思路要点:

  • 先把 n − 1 n-1 n−1 分解成 2 s ⋅ d 2^s \cdot d 2s⋅d,其中 d d d 是奇数
  • 对每个底数 a a a,计算 x = a d   m o d   n x = a^d \bmod n x=admodn
  • 如果 x = 1 x = 1 x=1 或 x = n − 1 x = n-1 x=n−1,则通过本轮测试
  • 否则重复平方 s − 1 s-1 s−1 次,如果某次得到 n − 1 n-1 n−1,则通过
  • 如果所有底数都通过,则 n n n 是质数(概率极高)
  • 对于 64 位整数,固定底数 { 2 , 3 , 5 , 7 , 11 , 13 , 17 , 19 , 23 , 29 , 31 , 37 } \{2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37\} {2,3,5,7,11,13,17,19,23,29,31,37} 可以保证正确性
  • 时间复杂度 O(k log³ n),k 是底数个数
  • 需要用 __int128 或快速乘防止乘法溢出
5.8.4 三种方法对比
方法 时间复杂度 适用场景
埃式筛 O(n log log n) 小数据、只判质数
欧拉筛 O(n) 大数据、需积性函数
米勒-拉宾 O(k log³ n) 单个大数(10^18)素性判断
试除法 O(√n) 单个数、n 较小
5.8.5 注意事项
  • 埃式筛内层从 i * i 开始,避免重复标记
  • 欧拉筛的 break 条件是 i % p == 0
  • 米勒-拉宾需要处理 n < 2、n == 2、n == 3 等边界
  • 米勒-拉宾的乘法可能溢出,要用 __int128 或快速乘
  • 固定底数对 64 位整数足够,但更大的数需要随机底数

5.9 裴蜀定理

适用场景: 线性组合最值、整数解存在性判断、GCD 相关博弈、扩展欧几里得的前置知识

5.9.1 基本原理

裴蜀定理(二元形式):

设 a , b a, b a,b 是不全为 0 的整数,则存在整数 x , y x, y x,y,使得:

a x + b y = gcd ⁡ ( a , b ) ax + by = \gcd(a, b) ax+by=gcd(a,b)

并且 gcd ⁡ ( a , b ) \gcd(a, b) gcd(a,b) 是 a x + b y ax + by ax+by 能表示的最小正整数。

推广到 n 元:

设 a 1 , a 2 , ... , a n a_1, a_2, \dots, a_n a1,a2,...,an 是不全为 0 的整数,则存在整数 x 1 , x 2 , ... , x n x_1, x_2, \dots, x_n x1,x2,...,xn,使得:

a 1 x 1 + a 2 x 2 + ⋯ + a n x n = gcd ⁡ ( a 1 , a 2 , ... , a n ) a_1 x_1 + a_2 x_2 + \cdots + a_n x_n = \gcd(a_1, a_2, \dots, a_n) a1x1+a2x2+⋯+anxn=gcd(a1,a2,...,an)

并且 gcd ⁡ ( a 1 , a 2 , ... , a n ) \gcd(a_1, a_2, \dots, a_n) gcd(a1,a2,...,an) 是 a 1 x 1 + ⋯ + a n x n a_1 x_1 + \cdots + a_n x_n a1x1+⋯+anxn 能表示的最小正整数。

关键结论:

  • a x + b y = c ax + by = c ax+by=c 有整数解    ⟺    gcd ⁡ ( a , b ) ∣ c \iff \gcd(a, b) \mid c ⟺gcd(a,b)∣c
  • a 1 x 1 + ⋯ + a n x n a_1 x_1 + \cdots + a_n x_n a1x1+⋯+anxn 能表示的所有值都是 gcd ⁡ ( a 1 , ... , a n ) \gcd(a_1, \dots, a_n) gcd(a1,...,an) 的倍数
  • 最小正整数就是 gcd ⁡ ( a 1 , ... , a n ) \gcd(a_1, \dots, a_n) gcd(a1,...,an)

时间复杂度: 求 GCD 用辗转相除法,O(log max|a_i|)。

5.9.2 基础模板(洛谷 P4549)

题意: 给定 n n n 个整数 a 1 , ... , a n a_1, \dots, a_n a1,...,an,求 S = ∑ a i x i > 0 S = \sum a_i x_i > 0 S=∑aixi>0 的最小值。

思路: 根据裴蜀定理,答案就是 gcd ⁡ ( ∣ a 1 ∣ , ∣ a 2 ∣ , ... , ∣ a n ∣ ) \gcd(|a_1|, |a_2|, \dots, |a_n|) gcd(∣a1∣,∣a2∣,...,∣an∣)。

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

class Solver {
public:
    int run() {
        int n;
        cin >> n;
        int ans = 0;
        for (int i = 0; i < n; i++) {
            int x;
            cin >> x;
            ans = gcd(abs(x), ans);
        }
        cout << ans << '\n';
        return 0;
    }

private:
    int gcd(int a, int b) {
        return b == 0 ? a : gcd(b, a % b);
    }
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    Solver solver;
    return solver.run();
}

思路要点:

  • 答案就是所有 ∣ a i ∣ |a_i| ∣ai∣ 的 GCD
  • 注意 a i a_i ai 可能为负数,要取绝对值
  • 初始 a n s = 0 ans = 0 ans=0,因为 gcd ⁡ ( 0 , x ) = x \gcd(0, x) = x gcd(0,x)=x
  • 时间复杂度 O(n log max|a_i|)
5.9.3 应用一:修理宝塔(HDU 5512)

题意: 有编号 1 ∼ n 1 \sim n 1∼n 的宝塔, a a a 号和 b b b 号已经修好。两人轮流修塔,每次可以选择 j + k j+k j+k 号或 j − k j-k j−k 号塔进行修理,其中 j , k j, k j,k 是任意两个已经修好的塔。谁先修完所有塔谁赢。问先手是否必胜。

思路:

  • 根据裴蜀定理,能修好的塔的编号一定是 gcd ⁡ ( a , b ) \gcd(a, b) gcd(a,b) 的倍数
  • 所以能修好的塔的数量为 ⌊ n / gcd ⁡ ( a , b ) ⌋ \lfloor n / \gcd(a, b) \rfloor ⌊n/gcd(a,b)⌋
  • 减去已经修好的 2 个塔,剩余可修的塔数为 ⌊ n / gcd ⁡ ( a , b ) ⌋ − 2 \lfloor n / \gcd(a, b) \rfloor - 2 ⌊n/gcd(a,b)⌋−2
  • 这是一个取石子游戏,先手必胜    ⟺    \iff ⟺ 剩余数量为奇数

代码:

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

class Solver {
public:
    int run() {
        int T;
        cin >> T;
        for (int t = 1; t <= T; t++) {
            int n, a, b;
            cin >> n >> a >> b;
            cout << "Case #" << t << ": ";
            if (((n / gcd(a, b)) & 1) == 1) {
                cout << "Yuwgna\n";
            } else {
                cout << "Iaka\n";
            }
        }
        return 0;
    }

private:
    int gcd(int a, int b) {
        return b == 0 ? a : gcd(b, a % b);
    }
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    Solver solver;
    return solver.run();
}

思路要点:

  • 能修好的塔的编号都是 gcd ⁡ ( a , b ) \gcd(a, b) gcd(a,b) 的倍数
  • 总数 = ⌊ n / gcd ⁡ ( a , b ) ⌋ \lfloor n / \gcd(a, b) \rfloor ⌊n/gcd(a,b)⌋
  • 剩余 = 总数 − 2
  • 剩余为奇数则先手胜,否则后手胜
5.9.4 应用二:扩展欧几里得求特解

题意: 给定 a , b a, b a,b,求 a x + b y = gcd ⁡ ( a , b ) ax + by = \gcd(a, b) ax+by=gcd(a,b) 的一组整数解 ( x , y ) (x, y) (x,y)。

代码:

cpp 复制代码
class Solver {
public:
    // 扩展欧几里得:求 ax + by = gcd(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 -= a / b * x;
        return g;
    }
};

思路要点:

  • 递归到 b = 0 b = 0 b=0 时, x = 1 , y = 0 x = 1, y = 0 x=1,y=0, gcd ⁡ = a \gcd = a gcd=a
  • 回溯时更新 x , y x, y x,y: y − = ( a / b ) ⋅ x y -= (a / b) \cdot x y−=(a/b)⋅x
  • 返回值是 gcd ⁡ ( a , b ) \gcd(a, b) gcd(a,b)
  • 通解: x + k ⋅ ( b / g ) x + k \cdot (b / g) x+k⋅(b/g), y − k ⋅ ( a / g ) y - k \cdot (a / g) y−k⋅(a/g)
5.9.5 应用三:线性同余方程

题意: 求 a x ≡ c ( m o d m ) ax \equiv c \pmod{m} ax≡c(modm) 的最小正整数解。

思路:

  • 转化为 a x + m y = c ax + my = c ax+my=c
  • 有解    ⟺    gcd ⁡ ( a , m ) ∣ c \iff \gcd(a, m) \mid c ⟺gcd(a,m)∣c
  • 用 exgcd 求 a x + m y = gcd ⁡ ( a , m ) ax + my = \gcd(a, m) ax+my=gcd(a,m) 的特解,再乘以 c / gcd ⁡ ( a , m ) c / \gcd(a, m) c/gcd(a,m)
  • 通解: x + k ⋅ ( m / gcd ⁡ ( a , m ) ) x + k \cdot (m / \gcd(a, m)) x+k⋅(m/gcd(a,m)),取最小正整数

代码:

cpp 复制代码
class Solver {
public:
    // 求 ax ≡ c (mod m) 的最小正整数解
    long long solveCongruence(long long a, long long c, long long m) {
        long long x, y;
        long long g = exgcd(a, m, x, y);
        if (c % g != 0) return -1;   // 无解
        x = x * (c / g) % m;
        long long mod = m / g;
        x = (x % mod + mod) % mod;
        return x;
    }
};

思路要点:

  • 有解条件: gcd ⁡ ( a , m ) ∣ c \gcd(a, m) \mid c gcd(a,m)∣c
  • 特解乘 c / g c / g c/g 后取模
  • 最小正整数解在 [ 0 , m / g ) [0, m / g) [0,m/g) 范围内
5.9.6 应用四:线性丢番图方程

题意: 求 a x + b y = c ax + by = c ax+by=c 的所有整数解。

思路:

  • 有解    ⟺    gcd ⁡ ( a , b ) ∣ c \iff \gcd(a, b) \mid c ⟺gcd(a,b)∣c
  • 用 exgcd 求 a x + b y = gcd ⁡ ( a , b ) ax + by = \gcd(a, b) ax+by=gcd(a,b) 的特解 ( x 0 , y 0 ) (x_0, y_0) (x0,y0)
  • 原方程特解: x = x 0 ⋅ ( c / g ) x = x_0 \cdot (c / g) x=x0⋅(c/g), y = y 0 ⋅ ( c / g ) y = y_0 \cdot (c / g) y=y0⋅(c/g)
  • 通解: x + k ⋅ ( b / g ) x + k \cdot (b / g) x+k⋅(b/g), y − k ⋅ ( a / g ) y - k \cdot (a / g) y−k⋅(a/g)

代码:

cpp 复制代码
class Solver {
public:
    // 求 ax + by = c 的一组特解
    bool solveDiophantine(long long a, long long b, long long c,
                          long long &x, long long &y) {
        long long g = exgcd(a, b, x, y);
        if (c % g != 0) return false;
        x *= c / g;
        y *= c / g;
        return true;
    }
};

思路要点:

  • 有解条件: gcd ⁡ ( a , b ) ∣ c \gcd(a, b) \mid c gcd(a,b)∣c
  • 特解乘 c / g c / g c/g
  • 通解通过加减 ( b / g , − a / g ) (b/g, -a/g) (b/g,−a/g) 得到
5.9.7 裴蜀定理的推论
推论 说明
a x + b y = 1 ax + by = 1 ax+by=1 有解 ⟺    gcd ⁡ ( a , b ) = 1 \iff \gcd(a, b) = 1 ⟺gcd(a,b)=1
a x + b y = c ax + by = c ax+by=c 有解 ⟺    gcd ⁡ ( a , b ) ∣ c \iff \gcd(a, b) \mid c ⟺gcd(a,b)∣c
a 1 x 1 + ⋯ + a n x n = 1 a_1 x_1 + \cdots + a_n x_n = 1 a1x1+⋯+anxn=1 有解 ⟺    gcd ⁡ ( a 1 , ... , a n ) = 1 \iff \gcd(a_1, \dots, a_n) = 1 ⟺gcd(a1,...,an)=1
最小正整数 gcd ⁡ ( a 1 , ... , a n ) \gcd(a_1, \dots, a_n) gcd(a1,...,an)
所有可表示的值 都是 gcd ⁡ \gcd gcd 的倍数
5.9.8 时间复杂度总结
操作 复杂度
求 GCD O(log max|a_i|)
扩展欧几里得 O(log max|a|, |b|)
线性同余方程 O(log m)
线性丢番图方程 O(log max|a|, |b|)
5.9.9 注意事项
  • a i a_i ai 可能为负数,求 GCD 时要取绝对值

  • 初始 a n s = 0 ans = 0 ans=0,因为 gcd ⁡ ( 0 , x ) = x \gcd(0, x) = x gcd(0,x)=x

  • 扩展欧几里得中, x , y x, y x,y 要用引用传递

  • 线性同余方程取最小正整数解时要 (x % mod + mod) % mod

  • 裴蜀定理的结论是存在性,不保证解唯一

  • 多测试用例时注意清空变量

5.x 卡特兰数和组合数

5.x.1 卡特兰数(Catalan Number)

适用场景: 出栈序列计数、括号匹配、圆上连线、二叉树计数、路径计数、凸多边形三角划分等

5.x.1.1 基本原理

卡特兰数定义:

C n = 1 n + 1 ( 2 n n ) = ( 2 n n ) − ( 2 n n − 1 ) C_n = \frac{1}{n+1} \binom{2n}{n} = \binom{2n}{n} - \binom{2n}{n-1} Cn=n+11(n2n)=(n2n)−(n−12n)

前几项:

n 0 1 2 3 4 5 6 7 8
C_n 1 1 2 5 14 42 132 429 1430

核心思想: 卡特兰数描述的是"合法序列"的计数问题。典型特征:每一步有两种选择,且任意前缀满足某种约束。

5.x.1.2 四种公式

公式 1(组合数差):

C n = ( 2 n n ) − ( 2 n n − 1 ) C_n = \binom{2n}{n} - \binom{2n}{n-1} Cn=(n2n)−(n−12n)

公式 2(组合数除法):

C n = 1 n + 1 ( 2 n n ) C_n = \frac{1}{n+1} \binom{2n}{n} Cn=n+11(n2n)

公式 3(递推式):

C 0 = C 1 = 1 , C n = C n − 1 ⋅ 4 n − 2 n + 1 C_0 = C_1 = 1, \quad C_n = C_{n-1} \cdot \frac{4n-2}{n+1} C0=C1=1,Cn=Cn−1⋅n+14n−2

公式 4(卷积式):

C 0 = 1 , C n = ∑ i = 0 n − 1 C i ⋅ C n − 1 − i C_0 = 1, \quad C_n = \sum_{i=0}^{n-1} C_i \cdot C_{n-1-i} C0=1,Cn=i=0∑n−1Ci⋅Cn−1−i

5.x.1.3 基础模板(模数为质数)
cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

class Catalan {
public:
    static constexpr int MOD = 1000000007;
    static constexpr int MAXN = 1000005;
    long long fac[MAXN];    // 阶乘
    long long inv1[MAXN];   // 阶乘逆元
    long long inv2[MAXN];   // 连续数逆元
    long long power(long long x, long long p) {
        long long ans = 1;
        while (p > 0) {
            if (p & 1) ans = ans * x % MOD;
            x = x * x % MOD;
            p >>= 1;
        }
        return ans;
    }
    // 生成阶乘表 + 阶乘逆元表
    void build1(int n) {
        fac[0] = inv1[0] = 1;
        fac[1] = 1;
        for (int i = 2; i <= n; i++) {
            fac[i] = (long long)i * fac[i - 1] % MOD;
        }
        inv1[n] = power(fac[n], MOD - 2);
        for (int i = n - 1; i >= 1; i--) {
            inv1[i] = (long long)(i + 1) * inv1[i + 1] % MOD;
        }
    }
    // 生成连续数逆元表
    void build2(int n) {
        inv2[1] = 1;
        for (int i = 2; i <= n + 1; i++) {
            inv2[i] = MOD - inv2[MOD % i] * (MOD / i) % MOD;
        }
    }
    long long c(int n, int k) {
        return fac[n] * inv1[k] % MOD * inv1[n - k] % MOD;
    }
    // 公式1
    long long catalan1(int n) {
        build1(2 * n);
        return (c(2 * n, n) - c(2 * n, n - 1) + MOD) % MOD;
    }
    // 公式2
    long long catalan2(int n) {
        build1(2 * n);
        return c(2 * n, n) * power(n + 1, MOD - 2) % MOD;
    }
    // 公式3
    long long catalan3(int n) {
        build2(n);
        vector<long long> f(n + 1);
        f[0] = f[1] = 1;
        for (int i = 2; i <= n; i++) {
            f[i] = f[i - 1] * (4 * i - 2) % MOD * inv2[i + 1] % MOD;
        }
        return f[n];
    }
    // 公式4
    long long catalan4(int n) {
        vector<long long> f(n + 1);
        f[0] = f[1] = 1;
        for (int i = 2; i <= n; i++) {
            for (int l = 0, r = i - 1; l < i; l++, r--) {
                f[i] = (f[i] + f[l] * f[r] % MOD) % MOD;
            }
        }
        return f[n];
    }
};

思路要点:

  • 公式1、2 需要阶乘和逆元,O(n) 预处理
  • 公式3 只需要连续数逆元,O(n)
  • 公式4 是 O(n²),适合 n 小的情况
  • 模数必须是质数,才能用费马小定理求逆元
5.x.1.4 模数不为质数的情况(因子计数法)

问题: 求第 n 项卡特兰数,对 p 取模,p 可能不为质数。

思路: 用公式2 1 n + 1 ( 2 n n ) \frac{1}{n+1} \binom{2n}{n} n+11(n2n),但不能直接求逆元。改用质因子计数法:

  • 用欧拉筛求出最小质因子
  • 把分子分母的每个数分解成质因子,统计每个质因子的指数
  • 最后用快速幂计算乘积
cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

class Catalan {
public:
    static constexpr int MAXN = 2000001;
    int minpf[MAXN];    // 最小质因子,0 表示质数
    int prime[MAXN];    // 质数表
    int cnt;            // 质数个数
    int counts[MAXN];   // 因子计数
    // 欧拉筛
    void euler(int n) {
        fill(minpf + 2, minpf + n + 1, 0);
        cnt = 0;
        for (int i = 2; i <= n; i++) {
            if (minpf[i] == 0) prime[cnt++] = i;
            for (int j = 0; j < cnt; j++) {
                if (i * prime[j] > n) break;
                minpf[i * prime[j]] = prime[j];
                if (i % prime[j] == 0) break;
            }
        }
    }
    long long power(long long x, long long p, int mod) {
        long long ans = 1;
        while (p > 0) {
            if (p & 1) ans = ans * x % mod;
            x = x * x % mod;
            p >>= 1;
        }
        return ans;
    }
    // 公式2 + 因子计数法
    int compute(int n, int mod) {
        euler(2 * n);

        // 分母:n+1 到 2n+1 的因子计数为 -1
        // 分子:n+2 到 2n 的因子计数为 1
        fill(counts + 2, counts + n + 2, -1);
        fill(counts + n + 2, counts + 2 * n + 1, 1);

        // 从大到小分解合数
        for (int i = 2 * n; i >= 2; i--) {
            if (minpf[i] != 0) {
                counts[minpf[i]] += counts[i];
                counts[i / minpf[i]] += counts[i];
                counts[i] = 0;
            }
        }

        // 快速幂计算
        long long ans = 1;
        for (int i = 2; i <= 2 * n; i++) {
            if (counts[i] != 0) {
                ans = ans * power(i, counts[i], mod) % mod;
            }
        }
        return (int)ans;
    }
};

思路要点:

  • 模数可能不是质数,不能用费马小定理
  • 用质因子计数法,把除法转化为指数相减
  • 欧拉筛 O(n) 预处理最小质因子
  • 从大到小分解合数,累加计数
  • 最后用快速幂计算乘积
  • 经典题型:洛谷 P3200
5.x.1.5 经典应用
应用 答案 说明
n 个元素的出栈序列 C_n 进栈 1...n,出栈顺序数
n 对括号的合法匹配 C_n 任意前缀左括号 ≥ 右括号
n 个节点的二叉树形态 C_n 不同形态的二叉树
圆上 2n 个点连线不相交 C_n 洛谷 P1976
凸 n+2 边形的三角划分 C_n 不同划分方案
从 (0,0) 到 (n,n) 不越过对角线 C_n 路径计数
n 个数的连乘加括号方案 C_n 矩阵连乘
5.x.1.6 圆上连线(洛谷 P1976)

问题: 圆上有 2n 个点,用 n 条线段成对连接,任意两条线段不相交,求方案数。

思路: 固定一个点,它必须和某个点相连,把圆分成两部分,递归计算。

答案: C_n

cpp 复制代码
class Catalan {
public:
    // 模数为 10^8 + 7(注意不是 10^9 + 7)
    static constexpr int MOD = 100000007;
    static constexpr int MAXN = 1000001;
    long long fac[MAXN], inv[MAXN];
    void build(int n) {
        fac[0] = inv[0] = 1;
        fac[1] = 1;
        for (int i = 2; i <= n; i++) fac[i] = fac[i - 1] * i % MOD;
        inv[n] = power(fac[n], MOD - 2);
        for (int i = n - 1; i >= 1; i--) inv[i] = inv[i + 1] * (i + 1) % MOD;
    }
    long long c(int n, int k) {
        return fac[n] * inv[k] % MOD * inv[n - k] % MOD;
    }
    long long catalan(int n) {
        build(2 * n);
        return (c(2 * n, n) - c(2 * n, n - 1) + MOD) % MOD;
    }
};

思路要点:

  • 答案就是 C_n
  • 注意模数是 10^8 + 7,不是 10^9 + 7
  • 用公式1 或公式2
5.x.1.7 卡特兰数的推导

以出栈序列为例:

  • 进栈顺序固定为 1, 2, ..., n
  • 任意时刻,出栈的元素个数 ≤ 进栈的元素个数
  • 把进栈看作 +1,出栈看作 -1,则任意前缀和 ≥ 0
  • 从 (0,0) 到 (2n,0),不越过 x 轴的路径数
  • 总路径数 = ( 2 n n ) \binom{2n}{n} (n2n),非法路径数 = ( 2 n n − 1 ) \binom{2n}{n-1} (n−12n)
  • 所以 C n = ( 2 n n ) − ( 2 n n − 1 ) C_n = \binom{2n}{n} - \binom{2n}{n-1} Cn=(n2n)−(n−12n)

以括号匹配为例:

  • n 对括号,任意前缀左括号 ≥ 右括号
  • 同上,答案为 C_n

以二叉树为例:

  • n 个节点的二叉树,根节点固定,左右子树节点数分别为 i 和 n-1-i
  • 递推式: C n = ∑ i = 0 n − 1 C i ⋅ C n − 1 − i C_n = \sum_{i=0}^{n-1} C_i \cdot C_{n-1-i} Cn=∑i=0n−1Ci⋅Cn−1−i
5.x.1.8 卡特兰数的生成函数

生成函数:

C ( x ) = ∑ n = 0 ∞ C n x n = 1 − 1 − 4 x 2 x C(x) = \sum_{n=0}^{\infty} C_n x^n = \frac{1 - \sqrt{1 - 4x}}{2x} C(x)=n=0∑∞Cnxn=2x1−1−4x

满足方程:

C ( x ) = 1 + x ⋅ C ( x ) 2 C(x) = 1 + x \cdot C(x)^2 C(x)=1+x⋅C(x)2

推导: 由卷积式 C n = ∑ i = 0 n − 1 C i C n − 1 − i C_n = \sum_{i=0}^{n-1} C_i C_{n-1-i} Cn=∑i=0n−1CiCn−1−i 得到。

5.x.1.9 复杂度总结
公式 复杂度 适用场景
公式1 O(n) 模数为质数
公式2 O(n) 模数为质数
公式3 O(n) 模数为质数,只需连续逆元
公式4 O(n²) n 小,模数任意
因子计数法 O(n log n) 模数任意
5.x.1.10 注意事项
  • 卡特兰数增长很快,n 大时要用 long long 或取模
  • 模数为质数时可以用费马小定理求逆元
  • 模数不为质数时用因子计数法
  • 公式1 和公式2 等价,公式1 少一次快速幂
  • 公式3 需要连续数逆元表
  • 公式4 是 O(n²),适合 n ≤ 5000
  • 圆上连线问题模数可能不同,注意题目要求
  • 多组测试要清空数组
  • 卡特兰数的前几项要记熟:1, 1, 2, 5, 14, 42, 132, 429, 1430
5.x.2 二项式定理和二项式反演

适用场景: 组合恒等式、系数计算、错排问题、容斥原理、至少/恰好问题转化

5.x.2.1 二项式定理

核心公式:

( a + b ) n = ∑ k = 0 n ( n k ) a n − k b k (a + b)^n = \sum_{k=0}^{n} \binom{n}{k} a^{n-k} b^k (a+b)n=k=0∑n(kn)an−kbk

推广形式:

( a x + b y ) k = ∑ i = 0 k ( k i ) ( a x ) k − i ( b y ) i (ax + by)^k = \sum_{i=0}^{k} \binom{k}{i} (ax)^{k-i} (by)^i (ax+by)k=i=0∑k(ik)(ax)k−i(by)i

展开后 x n y m x^n y^m xnym 项的系数为(其中 n + m = k n + m = k n+m=k):

( k n ) a n b m \binom{k}{n} a^n b^m (nk)anbm

组合意义: 从 k k k 个 ( a x + b y ) (ax + by) (ax+by) 中选 n n n 个取 a x ax ax,其余 m m m 个取 b y by by。

5.x.2.2 杨辉三角与组合数

杨辉三角递推式:

( n k ) = ( n − 1 k − 1 ) + ( n − 1 k ) \binom{n}{k} = \binom{n-1}{k-1} + \binom{n-1}{k} (kn)=(k−1n−1)+(kn−1)

边界条件:

( n 0 ) = ( n n ) = 1 \binom{n}{0} = \binom{n}{n} = 1 (0n)=(nn)=1

代码(打印杨辉三角前 n 行):

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

class Solver {
public:
    int run() {
        int n;
        cin >> n;

        for (int i = 0; i < n; i++) {
            tri[i][0] = tri[i][i] = 1;
        }
        for (int i = 1; i < n; i++) {
            for (int j = 1; j < i; j++) {
                tri[i][j] = tri[i - 1][j] + tri[i - 1][j - 1];
            }
        }

        for (int i = 0; i < n; i++) {
            for (int j = 0; j <= i; j++) {
                cout << tri[i][j] << " ";
            }
            cout << '\n';
        }
        return 0;
    }

private:
    static constexpr int MAXN = 25;

    long long tri[MAXN][MAXN];
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    Solver solver;
    return solver.run();
}

思路要点:

  • 杨辉三角第 i i i 行第 j j j 个数就是 ( i j ) \binom{i}{j} (ji)
  • 递推式: ( n k ) = ( n − 1 k − 1 ) + ( n − 1 k ) \binom{n}{k} = \binom{n-1}{k-1} + \binom{n-1}{k} (kn)=(k−1n−1)+(kn−1)
  • 适用于 n n n 较小的情况( n ≤ 5000 n \le 5000 n≤5000)
  • 复杂度 O(n²)
5.x.2.3 组合数的计算方法
方法 复杂度 适用场景
杨辉三角递推 O(n²) n ≤ 5000,模数任意
阶乘 + 逆元 O(n) 预处理,O(1) 查询 模数为质数
卢卡斯定理 O(p log n) 模数为小质数
因子计数法 O(n log n) 模数任意

阶乘 + 逆元模板(模数为质数):

cpp 复制代码
class Solver {
public:
    static constexpr int MOD = 1000000007;
    static constexpr int MAXN = 1000005;
    long long fac[MAXN], inv[MAXN];
    long long power(long long x, long long p) {
        long long ans = 1;
        while (p > 0) {
            if (p & 1) ans = ans * x % MOD;
            x = x * x % MOD;
            p >>= 1;
        }
        return ans;
    }
    void build(int n) {
        fac[0] = inv[0] = 1;
        for (int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i % MOD;
        inv[n] = power(fac[n], MOD - 2);
        for (int i = n - 1; i >= 0; i--) inv[i] = inv[i + 1] * (i + 1) % MOD;
    }
    long long c(int n, int k) {
        if (k < 0 || k > n) return 0;
        return fac[n] * inv[k] % MOD * inv[n - k] % MOD;
    }
};

思路要点:

  • 模数为质数时,用费马小定理求逆元
  • 预处理 O(n),单次查询 O(1)
  • 注意边界:k < 0 或 k > n 时返回 0
5.x.2.4 二项式定理的应用

应用一:计算系数(洛谷 P1313)

问题: 求 ( a x + b y ) k (ax + by)^k (ax+by)k 展开后 x n y m x^n y^m xnym 项的系数( n + m = k n + m = k n+m=k)。

答案:

( k n ) a n b m   m o d   10007 \binom{k}{n} a^n b^m \bmod 10007 (nk)anbmmod10007

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

class Solver {
public:
    int run() {
        int a, b, k, n, m;
        cin >> a >> b >> k >> n >> m;
        build();
        long long ans = power(a, n) * power(b, m) % MOD * c(k, n) % MOD;
        cout << ans << '\n';
        return 0;
    }

private:
    static constexpr int MOD = 10007;
    static constexpr int MAXK = 1005;

    long long fac[MAXK], inv[MAXK];

    long long power(long long x, long long p) {
        long long ans = 1;
        while (p > 0) {
            if (p & 1) ans = ans * x % MOD;
            x = x * x % MOD;
            p >>= 1;
        }
        return ans;
    }

    void build() {
        fac[0] = inv[0] = 1;
        fac[1] = 1;
        for (int i = 2; i < MAXK; i++) fac[i] = fac[i - 1] * i % MOD;
        inv[MAXK - 1] = power(fac[MAXK - 1], MOD - 2);
        for (int i = MAXK - 2; i >= 0; i--) inv[i] = inv[i + 1] * (i + 1) % MOD;
    }

    long long c(int n, int k) {
        if (k < 0 || k > n) return 0;
        return fac[n] * inv[k] % MOD * inv[n - k] % MOD;
    }
};

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    Solver solver;
    return solver.run();
}

思路要点:

  • 直接用二项式定理
  • 答案 = ( k n ) a n b m   m o d   10007 \binom{k}{n} a^n b^m \bmod 10007 (nk)anbmmod10007
  • 注意模数是 10007,不是 1e9+7

应用二:错排问题(洛谷 P1595)

问题: n 个人各写一封信,每个人必须收到一封信且不能收到自己的,求方案数。

递推式:

D 1 = 0 , D 2 = 1 , D n = ( n − 1 ) ( D n − 1 + D n − 2 ) D_1 = 0, \quad D_2 = 1, \quad D_n = (n-1)(D_{n-1} + D_{n-2}) D1=0,D2=1,Dn=(n−1)(Dn−1+Dn−2)

通项公式(二项式反演):

D n = n ! ∑ i = 0 n ( − 1 ) i i ! D_n = n! \sum_{i=0}^{n} \frac{(-1)^i}{i!} Dn=n!i=0∑ni!(−1)i

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

class Solver {
public:
    // 递推法
    long long derangement1(int n) {
        vector<long long> dp(n + 1);
        dp[1] = 0;
        dp[2] = 1;
        for (int i = 3; i <= n; i++) {
            dp[i] = (i - 1) * (dp[i - 1] + dp[i - 2]);
        }
        return dp[n];
    }
    // 通项公式法(二项式反演)
    long long derangement2(int n) {
        long long facn = 1;
        for (int i = 1; i <= n; i++) facn *= i;

        long long ans = facn;
        long long faci = 1;
        for (int i = 1; i <= n; i++) {
            faci *= i;
            if (i & 1) ans -= facn / faci;
            else       ans += facn / faci;
        }
        return ans;
    }
};

思路要点:

  • 递推式: D n = ( n − 1 ) ( D n − 1 + D n − 2 ) D_n = (n-1)(D_{n-1} + D_{n-2}) Dn=(n−1)(Dn−1+Dn−2)
  • 通项公式用二项式反演推导
  • n 小时(n ≤ 20)直接用通项公式
  • 经典题型:洛谷 P1595
5.x.2.5 二项式反演

核心公式:

如果 f ( n ) = ∑ i = 0 n ( n i ) g ( i ) f(n) = \sum_{i=0}^{n} \binom{n}{i} g(i) f(n)=∑i=0n(in)g(i),则:

g ( n ) = ∑ i = 0 n ( − 1 ) n − i ( n i ) f ( i ) g(n) = \sum_{i=0}^{n} (-1)^{n-i} \binom{n}{i} f(i) g(n)=i=0∑n(−1)n−i(in)f(i)

更一般的形式:

f ( n ) = ∑ i = n m ( i n ) g ( i )    ⟺    g ( n ) = ∑ i = n m ( − 1 ) i − n ( i n ) f ( i ) f(n) = \sum_{i=n}^{m} \binom{i}{n} g(i) \iff g(n) = \sum_{i=n}^{m} (-1)^{i-n} \binom{i}{n} f(i) f(n)=i=n∑m(ni)g(i)⟺g(n)=i=n∑m(−1)i−n(ni)f(i)

组合意义: 二项式反演是容斥原理的代数形式,用于把"至少"转化为"恰好"。

5.x.2.6 二项式反演的典型应用

应用一:错排问题

设 f ( n ) f(n) f(n) 为 n 个人任意寄信的方案数( n ! n! n!), g ( n ) g(n) g(n) 为错排方案数。

由 f ( n ) = ∑ i = 0 n ( n i ) g ( i ) f(n) = \sum_{i=0}^{n} \binom{n}{i} g(i) f(n)=∑i=0n(in)g(i)(枚举有多少人收到自己的信),反演得:

g ( n ) = ∑ i = 0 n ( − 1 ) n − i ( n i ) f ( i ) = n ! ∑ i = 0 n ( − 1 ) i i ! g(n) = \sum_{i=0}^{n} (-1)^{n-i} \binom{n}{i} f(i) = n! \sum_{i=0}^{n} \frac{(-1)^i}{i!} g(n)=i=0∑n(−1)n−i(in)f(i)=n!i=0∑ni!(−1)i

应用二:恰好 k 个满足条件

问题: 给定 n 个元素,求恰好有 k 个满足某条件的方案数。

思路:

  • 设 f ( k ) f(k) f(k) 为"至少 k 个满足条件"的方案数
  • 设 g ( k ) g(k) g(k) 为"恰好 k 个满足条件"的方案数
  • 关系: f ( k ) = ∑ i = k n ( i k ) g ( i ) f(k) = \sum_{i=k}^{n} \binom{i}{k} g(i) f(k)=∑i=kn(ki)g(i)
  • 反演: g ( k ) = ∑ i = k n ( − 1 ) i − k ( i k ) f ( i ) g(k) = \sum_{i=k}^{n} (-1)^{i-k} \binom{i}{k} f(i) g(k)=∑i=kn(−1)i−k(ki)f(i)

应用三:球与盒子的计数

问题: n 个不同的球放入 m 个不同的盒子,每个盒子非空,求方案数。

思路:

  • 设 f ( m ) f(m) f(m) 为"至多 m 个盒子有球"的方案数( m n m^n mn)
  • 设 g ( m ) g(m) g(m) 为"恰好 m 个盒子有球"的方案数
  • 反演: g ( m ) = ∑ i = 0 m ( − 1 ) m − i ( m i ) i n g(m) = \sum_{i=0}^{m} (-1)^{m-i} \binom{m}{i} i^n g(m)=∑i=0m(−1)m−i(im)in

应用四:图上染色问题

问题: 用 k 种颜色给图染色,相邻点颜色不同,求方案数。

思路:

  • 用二项式反演把"恰好用 k 种颜色"转化为"至多用 k 种颜色"
  • 结合色多项式
5.x.2.7 分割的方法数(大厂笔试题)

问题: 给定数组 A,分成 B 和 C 两份,满足 A i = B i + C i Ai = Bi + Ci Ai=Bi+Ci,且 B 不降序、C 不升序,求方案数。

思路: 转化为杨辉三角(组合数)。

cpp 复制代码
class Solver {
public:
    static constexpr int MOD = 1000000007;
    long long power(long long x, long long p) {
        long long ans = 1;
        while (p > 0) {
            if (p & 1) ans = ans * x % MOD;
            x = x * x % MOD;
            p >>= 1;
        }
        return ans;
    }
    int ways2(vector<int>& arr) {
        int n = arr.size();
        int k = arr[0] - 1;
        for (int i = 1; i < n && k > 0; i++) {
            if (arr[i - 1] > arr[i]) {
                k -= arr[i - 1] - arr[i];
            }
        }
        if (k <= 0) return 0;
        return c(k + n - 1, n);
    }
};

思路要点:

  • 通过分析约束,问题转化为组合数 ( k + n − 1 n ) \binom{k+n-1}{n} (nk+n−1)
  • 其中 k 是初始可用空间减去下降量
  • 答案对 1e9+7 取模
5.x.2.8 二项式定理与二项式反演对比
项目 二项式定理 二项式反演
核心 ( a + b ) n (a+b)^n (a+b)n 展开 两个序列的互相推导
公式 ∑ ( n k ) a n − k b k \sum \binom{n}{k} a^{n-k} b^k ∑(kn)an−kbk f ( n ) = ∑ ( n i ) g ( i )    ⟺    g ( n ) = ∑ ( − 1 ) n − i ( n i ) f ( i ) f(n) = \sum \binom{n}{i} g(i) \iff g(n) = \sum (-1)^{n-i} \binom{n}{i} f(i) f(n)=∑(in)g(i)⟺g(n)=∑(−1)n−i(in)f(i)
用途 系数计算 容斥、恰好/至少转化
经典题 洛谷 P1313 洛谷 P1595
5.x.2.9 复杂度总结
操作 复杂度
杨辉三角预处理 O(n²)
阶乘 + 逆元预处理 O(n)
单次组合数查询 O(1)
二项式定理计算系数 O(log MOD)
二项式反演 O(n)
5.x.2.10 注意事项
  • 组合数计算前要确认模数是否为质数
  • 模数为质数时用费马小定理求逆元
  • 模数不为质数时用杨辉三角或因子计数法
  • 二项式反演要注意上下界
  • 错排问题 n 小时用通项公式,n 大时用递推式
  • 二项式定理中 a , b a, b a,b 可能很大,注意快速幂
  • 注意题目要求取模的数不一定是 1e9+7
  • 二项式反演常用于"至少 k 个"转"恰好 k 个"
  • 组合数下标从 0 开始,注意边界
  • 多组测试要清空数组

5.x.3 容斥定理

适用场景: 集合计数、至少/恰好问题、错排问题、莫比乌斯反演、二项式反演、图上染色计数等

5.x.3.1 基本原理

核心思想: 要计算多个集合并集的元素个数,先把所有集合的大小加起来,再减去两两交集的大小,再加上三三交集的大小,依次类推。

二元形式:

∣ A ∪ B ∣ = ∣ A ∣ + ∣ B ∣ − ∣ A ∩ B ∣ |A \cup B| = |A| + |B| - |A \cap B| ∣A∪B∣=∣A∣+∣B∣−∣A∩B∣

三元形式:

∣ A ∪ B ∪ C ∣ = ∣ A ∣ + ∣ B ∣ + ∣ C ∣ − ∣ A ∩ B ∣ − ∣ A ∩ C ∣ − ∣ B ∩ C ∣ + ∣ A ∩ B ∩ C ∣ |A \cup B \cup C| = |A| + |B| + |C| - |A \cap B| - |A \cap C| - |B \cap C| + |A \cap B \cap C| ∣A∪B∪C∣=∣A∣+∣B∣+∣C∣−∣A∩B∣−∣A∩C∣−∣B∩C∣+∣A∩B∩C∣

一般形式:

∣ ⋃ i = 1 n A i ∣ = ∑ ∅ ≠ S ⊆ { 1 , ... , n } ( − 1 ) ∣ S ∣ − 1 ∣ ⋂ i ∈ S A i ∣ \left| \bigcup_{i=1}^{n} A_i \right| = \sum_{\emptyset \ne S \subseteq \{1, \dots, n\}} (-1)^{|S| - 1} \left| \bigcap_{i \in S} A_i \right| i=1⋃nAi =∅=S⊆{1,...,n}∑(−1)∣S∣−1 i∈S⋂Ai

等价写法:

∣ ⋃ i = 1 n A i ∣ = ∑ k = 1 n ( − 1 ) k − 1 ∑ ∣ S ∣ = k ∣ ⋂ i ∈ S A i ∣ \left| \bigcup_{i=1}^{n} A_i \right| = \sum_{k=1}^{n} (-1)^{k-1} \sum_{|S| = k} \left| \bigcap_{i \in S} A_i \right| i=1⋃nAi =k=1∑n(−1)k−1∣S∣=k∑ i∈S⋂Ai

补集形式(最常用):

∣ ⋂ i = 1 n A i ‾ ∣ = ∣ U ∣ − ∣ ⋃ i = 1 n A i ∣ = ∑ S ⊆ { 1 , ... , n } ( − 1 ) ∣ S ∣ ∣ ⋂ i ∈ S A i ∣ \left| \bigcap_{i=1}^{n} \overline{A_i} \right| = |U| - \left| \bigcup_{i=1}^{n} A_i \right| = \sum_{S \subseteq \{1, \dots, n\}} (-1)^{|S|} \left| \bigcap_{i \in S} A_i \right| i=1⋂nAi =∣U∣− i=1⋃nAi =S⊆{1,...,n}∑(−1)∣S∣ i∈S⋂Ai

关键理解:

  • 容斥原理的本质是对每个元素被计算的次数进行修正
  • 一个属于恰好 k k k 个集合的元素,在容斥公式中会被计算 ∑ i = 1 k ( − 1 ) i − 1 ( k i ) = 1 \sum_{i=1}^{k} (-1)^{i-1} \binom{k}{i} = 1 ∑i=1k(−1)i−1(ik)=1 次
  • 所以每个元素恰好被计算一次
5.x.3.2 基础模板(二进制枚举)
  • 模板代码如下:
cpp 复制代码
#include <bits/stdc++.h>
using namespace std;

class Solver {
public:
    // 计算 n 个集合的并集大小
    // sets[i] 是第 i 个集合的元素集合
    long long inclusionExclusion(vector<set<int>>& sets) {
        int n = sets.size();
        long long ans = 0;

        // 枚举所有非空子集
        for (int mask = 1; mask < (1 << n); mask++) {
            // 求交集
            set<int> inter;
            bool first = true;
            for (int i = 0; i < n; i++) {
                if (mask & (1 << i)) {
                    if (first) {
                        inter = sets[i];
                        first = false;
                    } else {
                        set<int> tmp;
                        for (int x : inter) {
                            if (sets[i].count(x)) tmp.insert(x);
                        }
                        inter = tmp;
                    }
                }
            }
            // 容斥符号
            int bits = __builtin_popcount(mask);
            if (bits & 1) ans += inter.size();
            else          ans -= inter.size();
        }
        return ans;
    }
};

思路要点:

  • 枚举所有非空子集 mask
  • 求子集对应集合的交集
  • 根据子集大小奇偶性决定加减
  • 时间复杂度 O(2^n × n)
5.x.3.3 经典应用一:错排问题

问题: n 个人各写一封信,每个人必须收到一封信且不能收到自己的,求方案数。

思路: 设 A i A_i Ai 为"第 i 个人收到自己的信"的排列集合,则错排方案数为:

D n = ∣ ⋂ i = 1 n A i ‾ ∣ = ∑ k = 0 n ( − 1 ) k ( n k ) ( n − k ) ! D_n = \left| \bigcap_{i=1}^{n} \overline{A_i} \right| = \sum_{k=0}^{n} (-1)^k \binom{n}{k} (n-k)! Dn= i=1⋂nAi =k=0∑n(−1)k(kn)(n−k)!

代码:

cpp 复制代码
class Solver {
public:
    long long derangement(int n) {
        long long facn = 1;
        for (int i = 1; i <= n; i++) facn *= i;

        long long ans = facn;   // k = 0 项
        long long faci = 1;
        for (int i = 1; i <= n; i++) {
            faci *= i;
            if (i & 1) ans -= facn / faci;
            else       ans += facn / faci;
        }
        return ans;
    }
};

思路要点:

  • ∣ A i ∣ |A_i| ∣Ai∣:固定第 i 个人,其余任意排列,共 ( n − 1 ) ! (n-1)! (n−1)!
  • ∣ A i ∩ A j ∣ |A_i \cap A_j| ∣Ai∩Aj∣:固定 i 和 j,共 ( n − 2 ) ! (n-2)! (n−2)!
  • 容斥得 D n = ∑ k = 0 n ( − 1 ) k ( n k ) ( n − k ) ! D_n = \sum_{k=0}^{n} (-1)^k \binom{n}{k} (n-k)! Dn=∑k=0n(−1)k(kn)(n−k)!
  • 经典题型:洛谷 P1595
5.x.3.4 经典应用二:至少 / 恰好问题

问题: 给定 n 个元素,求恰好有 k 个满足某条件的方案数。

思路:

  • 设 f ( k ) f(k) f(k) 为"至少 k 个满足条件"的方案数
  • 设 g ( k ) g(k) g(k) 为"恰好 k 个满足条件"的方案数
  • 关系: f ( k ) = ∑ i = k n ( i k ) g ( i ) f(k) = \sum_{i=k}^{n} \binom{i}{k} g(i) f(k)=∑i=kn(ki)g(i)
  • 反演(二项式反演): g ( k ) = ∑ i = k n ( − 1 ) i − k ( i k ) f ( i ) g(k) = \sum_{i=k}^{n} (-1)^{i-k} \binom{i}{k} f(i) g(k)=∑i=kn(−1)i−k(ki)f(i)

代码:

cpp 复制代码
class Solver {
public:
    // 已知 f[k] = 至少 k 个满足条件的方案数
    // 求 g[k] = 恰好 k 个满足条件的方案数
    vector<long long> exactFromAtLeast(vector<long long>& f) {
        int n = f.size() - 1;
        vector<long long> g(n + 1, 0);
        for (int k = 0; k <= n; k++) {
            for (int i = k; i <= n; i++) {
                long long term = c(i, k) * f[i] % MOD;
                if ((i - k) & 1) g[k] = (g[k] - term + MOD) % MOD;
                else             g[k] = (g[k] + term) % MOD;
            }
        }
        return g;
    }
};

思路要点:

  • 二项式反演是容斥原理的代数形式
  • 常用于"至少"转"恰好"
  • 经典题型:洛谷 P4859、CF 285E
5.x.3.5 经典应用三:球与盒子的计数

问题: n 个不同的球放入 m 个不同的盒子,每个盒子非空,求方案数。

思路:

  • 设 f ( m ) f(m) f(m) 为"至多 m 个盒子有球"的方案数( m n m^n mn)
  • 设 g ( m ) g(m) g(m) 为"恰好 m 个盒子有球"的方案数
  • 反演: g ( m ) = ∑ i = 0 m ( − 1 ) m − i ( m i ) i n g(m) = \sum_{i=0}^{m} (-1)^{m-i} \binom{m}{i} i^n g(m)=∑i=0m(−1)m−i(im)in

代码:

cpp 复制代码
class Solver {
public:
    // n 个不同的球放入 m 个不同的盒子,每个盒子非空
    long long ballBox(int n, int m) {
        long long ans = 0;
        for (int i = 0; i <= m; i++) {
            long long term = c(m, i) * power(i, n) % MOD;
            if ((m - i) & 1) ans = (ans - term + MOD) % MOD;
            else             ans = (ans + term) % MOD;
        }
        return ans;
    }
};

思路要点:

  • f ( i ) = i n f(i) = i^n f(i)=in:每个球有 i 种选择
  • 容斥得 g ( m ) = ∑ i = 0 m ( − 1 ) m − i ( m i ) i n g(m) = \sum_{i=0}^{m} (-1)^{m-i} \binom{m}{i} i^n g(m)=∑i=0m(−1)m−i(im)in
  • 这就是第二类斯特林数 乘以 m ! m! m!
  • 经典题型:洛谷 P5824、CF 1342E
5.x.3.6 经典应用四:图上染色计数

问题: 用 k 种颜色给图染色,相邻点颜色不同,求方案数。

思路:

  • 设 f ( k ) f(k) f(k) 为"用至多 k 种颜色"的染色方案数
  • 设 g ( k ) g(k) g(k) 为"恰好用 k 种颜色"的染色方案数
  • 反演: g ( k ) = ∑ i = 0 k ( − 1 ) k − i ( k i ) f ( i ) g(k) = \sum_{i=0}^{k} (-1)^{k-i} \binom{k}{i} f(i) g(k)=∑i=0k(−1)k−i(ik)f(i)

代码:

cpp 复制代码
class Solver {
public:
    // f[i] = 用至多 i 种颜色染色的方案数
    // 求 g[k] = 恰好用 k 种颜色的方案数
    long long exactColors(vector<long long>& f, int k) {
        long long ans = 0;
        for (int i = 0; i <= k; i++) {
            long long term = c(k, i) * f[i] % MOD;
            if ((k - i) & 1) ans = (ans - term + MOD) % MOD;
            else             ans = (ans + term) % MOD;
        }
        return ans;
    }
};

思路要点:

  • f ( i ) f(i) f(i) 通常通过色多项式或 DP 求出
  • 容斥得恰好用 k 种颜色的方案数
  • 经典题型:洛谷 P4128、CF 111D
5.x.3.7 经典应用五:莫比乌斯反演

核心思想: 莫比乌斯反演是容斥原理在数论上的推广。

莫比乌斯函数:

μ ( n ) = { 1 n = 1 ( − 1 ) k n 是 k 个不同质数的乘积 0 n 含有平方因子 \mu(n) = \begin{cases} 1 & n = 1 \\ (-1)^k & n \text{ 是 } k \text{ 个不同质数的乘积} \\ 0 & n \text{ 含有平方因子} \end{cases} μ(n)=⎩ ⎨ ⎧1(−1)k0n=1n 是 k 个不同质数的乘积n 含有平方因子

莫比乌斯反演公式:

如果 f ( n ) = ∑ d ∣ n g ( d ) f(n) = \sum_{d \mid n} g(d) f(n)=∑d∣ng(d),则:

g ( n ) = ∑ d ∣ n μ ( d ) f ( n / d ) = ∑ d ∣ n μ ( n / d ) f ( d ) g(n) = \sum_{d \mid n} \mu(d) f(n/d) = \sum_{d \mid n} \mu(n/d) f(d) g(n)=d∣n∑μ(d)f(n/d)=d∣n∑μ(n/d)f(d)

代码:

cpp 复制代码
class Solver {
public:
    static constexpr int MAXN = 1000005;
    int mu[MAXN], prime[MAXN], cnt;
    bool vis[MAXN];
    // 线性筛求莫比乌斯函数
    void buildMu(int n) {
        mu[1] = 1;
        for (int i = 2; i <= n; i++) {
            if (!vis[i]) {
                prime[cnt++] = i;
                mu[i] = -1;
            }
            for (int j = 0; j < cnt && i * prime[j] <= n; j++) {
                vis[i * prime[j]] = true;
                if (i % prime[j] == 0) {
                    mu[i * prime[j]] = 0;
                    break;
                }
                mu[i * prime[j]] = -mu[i];
            }
        }
    }
};

思路要点:

  • 莫比乌斯反演是容斥在数论上的体现
  • 常用于 gcd ⁡ \gcd gcd 相关计数
  • 经典题型:洛谷 P2522、洛谷 P3455
5.x.3.8 容斥原理的常见形式
形式 公式 用途
并集 $\sum (-1)^{ S
补集 $\sum (-1)^{ S
二项式反演 g ( n ) = ∑ ( − 1 ) n − i ( n i ) f ( i ) g(n) = \sum (-1)^{n-i} \binom{n}{i} f(i) g(n)=∑(−1)n−i(in)f(i) 恰好 k 个
莫比乌斯反演 g ( n ) = ∑ d ∣ n μ ( d ) f ( n / d ) g(n) = \sum_{d \mid n} \mu(d) f(n/d) g(n)=∑d∣nμ(d)f(n/d) 数论计数
子集反演 g ( S ) = ∑ T ⊆ S ( − 1 ) ∣ S ∣ − ∣ T ∣ f ( T ) g(S) = \sum_{T \subseteq S} (-1)^{|S|-|T|} f(T) g(S)=∑T⊆S(−1)∣S∣−∣T∣f(T) 子集计数
5.x.3.9 复杂度总结
方法 复杂度 适用场景
二进制枚举 O(2^n × n) n ≤ 20
二项式反演 O(n²) n ≤ 5000
莫比乌斯反演 O(n log n) 数论计数
子集反演 O(3^n) 子集 DP
5.x.3.10 注意事项
  • 容斥原理的核心是"加奇减偶"
  • 补集形式常用于"至少/恰好"问题
  • 二项式反演是容斥的代数形式
  • 莫比乌斯反演是容斥的数论形式
  • 二进制枚举适用于 n 较小的情况
  • 容斥公式中的组合数要用 O(1) 查询
  • 注意取模,减法后要 + MOD 再 % MOD
  • 多组测试要清空数组
  • 容斥常与 DP、组合数、莫比乌斯函数结合
  • 经典题型:错排、球盒、染色、 gcd ⁡ \gcd gcd 计数

5.x.4 技巧---同余原理

5.x.4.1 核心概念与性质

定义 :设 m m m 为正整数,若 a − b a - b a−b 是 m m m 的倍数,则称 a a a 与 b b b 模 m m m 同余,记作 a ≡ b ( m o d m ) a \equiv b \pmod{m} a≡b(modm)。

基本性质(加减乘运算与等式类似):

  • 自反性 : a ≡ a ( m o d m ) a \equiv a \pmod{m} a≡a(modm)
  • 对称性 :若 a ≡ b ( m o d m ) a \equiv b \pmod{m} a≡b(modm),则 b ≡ a ( m o d m ) b \equiv a \pmod{m} b≡a(modm)
  • 传递性 :若 a ≡ b ( m o d m ) a \equiv b \pmod{m} a≡b(modm) 且 b ≡ c ( m o d m ) b \equiv c \pmod{m} b≡c(modm),则 a ≡ c ( m o d m ) a \equiv c \pmod{m} a≡c(modm)
  • 加减性 :若 a ≡ b ( m o d m ) a \equiv b \pmod{m} a≡b(modm) 且 c ≡ d ( m o d m ) c \equiv d \pmod{m} c≡d(modm),则 a ± c ≡ b ± d ( m o d m ) a \pm c \equiv b \pm d \pmod{m} a±c≡b±d(modm)
  • 乘性 :若 a ≡ b ( m o d m ) a \equiv b \pmod{m} a≡b(modm) 且 c ≡ d ( m o d m ) c \equiv d \pmod{m} c≡d(modm),则 a c ≡ b d ( m o d m ) ac \equiv bd \pmod{m} ac≡bd(modm)
  • 幂性 :若 a ≡ b ( m o d m ) a \equiv b \pmod{m} a≡b(modm),则 a n ≡ b n ( m o d m ) a^n \equiv b^n \pmod{m} an≡bn(modm)

核心思想:同余运算可以像普通等式一样进行加减乘运算,唯独除法需要求逆元。这一性质使得大数取模问题可以转化为小数运算,是数论算法的基础。

负数取模的统一处理:

在竞赛中,经常需要处理负数取模的情况。C++ 中 a % m 的结果符号与 a 相同,因此需要统一转成非负余数:

cpp 复制代码
// 将 a 转为 [0, m) 范围内的非负余数
long long mod(long long a, long long m) {
    return (a % m + m) % m;
}
5.x.4.2 常见应用
5.x.4.2.1 大数取模与快速幂

利用同余性质,计算 a b   m o d   m a^b \bmod m abmodm 时,底数 a a a 可以先对 m m m 取模,指数运算过程中每一步都对 m m m 取模,避免大数溢出。

经典例题 :求 2004 2004 2004^{2004} 20042004 除以 7 的余数。因 2004 ≡ 1 ( m o d 7 ) 2004 \equiv 1 \pmod{7} 2004≡1(mod7),故 2004 2004 ≡ 1 2004 ≡ 1 ( m o d 7 ) 2004^{2004} \equiv 1^{2004} \equiv 1 \pmod{7} 20042004≡12004≡1(mod7)。

cpp 复制代码
class ModPow {
public:
    // 快速幂:计算 a^b mod m
    long long pow_mod(long long a, long long b, long long m) {
        long long res = 1;
        a = mod(a, m);
        while (b > 0) {
            if (b & 1) res = res * a % m;
            a = a * a % m;
            b >>= 1;
        }
        return res;
    }

    long long mod(long long a, long long m) {
        return (a % m + m) % m;
    }
};

复杂度:O(log b)。

5.x.4.2.2 前缀和优化:k 倍区间

问题 :求数列中有多少个连续子区间的和是 k k k 的倍数。

同余转化 :区间 i , j i, j i,j 的和为 k k k 的倍数,等价于前缀和 s u m j ≡ s u m i − 1 ( m o d k ) sumj \equiv sumi-1 \pmod{k} sumj≡sumi−1(modk)。

做法:

  • 计算前缀和数组 s u m 0.. n sum0..n sum0..n 对 k k k 取模的余数。
  • 用哈希表记录每个余数出现的次数 c n t r cntr cntr。
  • 对于每个余数 r r r,从 c n t r cntr cntr 个位置中任选两个作为区间端点,贡献为 ( c n t r 2 ) \binom{cntr}{2} (2cntr)。
cpp 复制代码
class KMultipleRange {
public:
    // 统计有多少个连续子区间的和是 k 的倍数
    long long countKMultiple(vector<int>& nums, int k) {
        int n = nums.size();
        unordered_map<int, int> cnt;
        cnt[0] = 1;  // 空前缀,余数为 0

        long long ans = 0;
        int prefix = 0;
        for (int i = 0; i < n; i++) {
            prefix = (prefix + nums[i]) % k;
            ans += cnt[prefix];  // 与之前同余的前缀构成 k 倍区间
            cnt[prefix]++;
        }
        return ans;
    }
};

复杂度:O(n),空间 O(k)。

变体:如果数组中的数可能为负数,取模后需要统一转成非负余数:

cpp 复制代码
prefix = ((prefix + nums[i]) % k + k) % k;
5.x.4.2.3 最长 k 倍区间

问题 :找出最长的连续子区间,其和是 k k k 的倍数。

做法 :正序遍历记录每个余数第一次 出现的位置,倒序遍历记录每个余数最后一次出现的位置,取差值最大者。

cpp 复制代码
class LongestKMultiple {
public:
    // 找出最长的连续子区间,其和是 k 的倍数
    int longestKMultiple(vector<int>& nums, int k) {
        int n = nums.size();
        vector<int> first(k, -1), last(k, -1);
        first[0] = 0;  // 空前缀,余数为 0 的位置为 0

        int prefix = 0;
        for (int i = 1; i <= n; i++) {
            prefix = (prefix + nums[i-1]) % k;
            if (first[prefix] == -1) first[prefix] = i;
            last[prefix] = i;
        }

        int ans = 0;
        for (int r = 0; r < k; r++) {
            if (first[r] != -1) ans = max(ans, last[r] - first[r]);
        }
        return ans;
    }
};

复杂度:O(n + k)。

5.x.4.2.4 第 N 个神奇数字(二分 + 容斥)

问题 :能被 a a a 或 b b b 整除的数称为"神奇数字",求第 n n n 个神奇数字。

核心公式 :不超过 x x x 的神奇数字个数为:

f ( x ) = ⌊ x a ⌋ + ⌊ x b ⌋ − ⌊ x l c m ( a , b ) ⌋ f(x) = \left\lfloor \frac{x}{a} \right\rfloor + \left\lfloor \frac{x}{b} \right\rfloor - \left\lfloor \frac{x}{\mathrm{lcm}(a,b)} \right\rfloor f(x)=⌊ax⌋+⌊bx⌋−⌊lcm(a,b)x⌋

其中 l c m ( a , b ) = a × b gcd ⁡ ( a , b ) \mathrm{lcm}(a,b) = \frac{a \times b}{\gcd(a,b)} lcm(a,b)=gcd(a,b)a×b。

为什么减去 ⌊ x / l c m ⌋ \lfloor x / \mathrm{lcm} \rfloor ⌊x/lcm⌋ :因为既能被 a a a 整除又能被 b b b 整除的数被加了两次,需要减去一次。这些数恰好是 l c m ( a , b ) \mathrm{lcm}(a,b) lcm(a,b) 的倍数。

二分答案 : f ( x ) f(x) f(x) 单调递增,二分找到最小的 x x x 使 f ( x ) ≥ n f(x) \ge n f(x)≥n。

cpp 复制代码
class NthMagicalNumber {
public:
    int nthMagicalNumber(int n, int a, int b) {
        const int MOD = 1e9 + 7;
        long long lcm = (long long)a / gcd(a, b) * b;
        long long l = 0, r = (long long)(a + b) * n;

        while (l < r) {
            long long mid = l + (r - l) / 2;
            long long cnt = mid / a + mid / b - mid / lcm;
            if (cnt >= n) r = mid;
            else l = mid + 1;
        }
        return l % MOD;
    }

    long long gcd(long long a, long long b) {
        return b == 0 ? a : gcd(b, a % b);
    }
};

复杂度:O(log n)。

5.x.4.2.5 环形问题:断环为链

问题 :环形数组上求满足条件的区间个数(如区间和为 m m m 的倍数)。

做法 :将数组复制一份接在后面,转化为长度 2 n 2n 2n 的线性问题,再用前缀和同余统计。

cpp 复制代码
class CircularKMultiple {
public:
    // 环形数组上,统计有多少个连续子区间的和是 k 的倍数
    // 注意:环形数组上的区间长度不能超过 n
    long long countCircularKMultiple(vector<int>& nums, int k) {
        int n = nums.size();
        vector<int> extended(2 * n);
        for (int i = 0; i < 2 * n; i++) {
            extended[i] = nums[i % n];
        }

        long long ans = 0;
        unordered_map<int, int> cnt;
        cnt[0] = 1;
        int prefix = 0;
        // 滑动窗口:维护长度为 n 的窗口内的前缀和余数
        for (int i = 0; i < 2 * n; i++) {
            prefix = (prefix + extended[i]) % k;
            if (i >= n) {
                // 移除离开窗口的前缀
                int old_prefix = 0;
                for (int j = 0; j <= i - n; j++) {
                    old_prefix = (old_prefix + extended[j]) % k;
                }
                cnt[old_prefix]--;
                if (cnt[old_prefix] == 0) cnt.erase(old_prefix);
            }
            ans += cnt[prefix];
            cnt[prefix]++;
        }
        return ans;
    }
};
5.x.4.2.6 同余最短路

问题:给定若干个数,问用它们能组合出多少个不同的数(或最小不能组合出的数)。

同余建模 :设最小的数为 a a a,以模 a a a 的余数 0 , 1 , ... , a − 1 0, 1, \ldots, a-1 0,1,...,a−1 为节点建图。

  • 对于每个数 b i b_i bi,从节点 r r r 向节点 ( r + b i )   m o d   a (r + b_i) \bmod a (r+bi)moda 连一条边,权值为 b i b_i bi。
  • 跑最短路, d i s t r distr distr 表示能凑出模 a a a 余 r r r 的最小数值。
cpp 复制代码
class ModShortestPath {
    const long long INF = 1e18;

public:
    // 给定若干数,求它们能组合出的最小不能表示的数
    // 或者统计能组合出的数的个数
    long long solve(vector<int>& nums) {
        int n = nums.size();
        sort(nums.begin(), nums.end());
        int a = nums[0];  // 最小的数作为模数

        vector<long long> dist(a, INF);
        vector<bool> vis(a, false);
        priority_queue<pair<long long, int>, vector<pair<long long, int>>, greater<>> pq;

        dist[0] = 0;
        pq.push({0, 0});

        while (!pq.empty()) {
            auto [d, u] = pq.top(); pq.pop();
            if (vis[u]) continue;
            vis[u] = true;

            for (int i = 1; i < n; i++) {
                int v = (u + nums[i]) % a;
                long long nd = d + nums[i];
                if (nd < dist[v]) {
                    dist[v] = nd;
                    pq.push({nd, v});
                }
            }
        }

        // 统计能组合出的数的个数(以某个上界为限制)
        long long ans = 0;
        for (int r = 0; r < a; r++) {
            if (dist[r] < INF) {
                // 具体统计方式取决于题目
            }
        }
        return ans;
    }
};

经典例题:P3403 跳楼机、P2371 墨墨的等式。

5.x.4.2.7 高次同余方程

问题 :求解 x a ≡ b ( m o d p ) x^a \equiv b \pmod{p} xa≡b(modp),其中 p p p 为质数。

做法:利用原根和离散对数(BSGS 算法)转化为线性同余方程。

cpp 复制代码
class DiscreteLog {
public:
    // BSGS:求最小的 x 使得 a^x ≡ b (mod p)
    // 要求 gcd(a, p) = 1
    long long bsgs(long long a, long long b, long long p) {
        a %= p; b %= p;
        if (b == 1) return 0;
        if (a == 0) return b == 0 ? 1 : -1;

        long long m = ceil(sqrt(p));
        unordered_map<long long, long long> mp;

        // 预处理 a^j
        long long cur = 1;
        for (long long j = 0; j < m; j++) {
            mp[cur] = j;
            cur = cur * a % p;
        }

        // 枚举 i,查找 b * (a^{-m})^i
        long long inv_am = pow_mod(a, m * (p - 2) % (p - 1), p);
        cur = b;
        for (long long i = 0; i <= m; i++) {
            if (mp.count(cur)) {
                return i * m + mp[cur];
            }
            cur = cur * inv_am % p;
        }
        return -1;
    }

    long long pow_mod(long long a, long long b, long long m) {
        long long res = 1;
        while (b) {
            if (b & 1) res = res * a % m;
            a = a * a % m;
            b >>= 1;
        }
        return res;
    }
};
5.x.4.2.8 组合数取模

问题 :计算 ( n k )   m o d   p \binom{n}{k} \bmod p (kn)modp。

做法:预处理阶乘和阶乘逆元,单次查询 O(1)。

cpp 复制代码
class Combination {
    static const int MOD = 1e9 + 7;
    static const int MAXN = 1000005;
    long long fact[MAXN], inv_fact[MAXN];

public:
    void init(int n) {
        fact[0] = 1;
        for (int i = 1; i <= n; i++) fact[i] = fact[i-1] * i % MOD;
        inv_fact[n] = pow_mod(fact[n], MOD - 2, MOD);
        for (int i = n - 1; i >= 0; i--) inv_fact[i] = inv_fact[i+1] * (i+1) % MOD;
    }

    long long C(int n, int k) {
        if (k < 0 || k > n) return 0;
        return fact[n] * inv_fact[k] % MOD * inv_fact[n-k] % MOD;
    }

    long long pow_mod(long long a, long long b, long long m) {
        long long res = 1;
        while (b) {
            if (b & 1) res = res * a % m;
            a = a * a % m;
            b >>= 1;
        }
        return res;
    }
};

卢卡斯定理 ( n n n 很大, p p p 较小且为质数):

( n k ) ≡ ( n   m o d   p k   m o d   p ) ⋅ ( ⌊ n / p ⌋ ⌊ k / p ⌋ ) ( m o d p ) \binom{n}{k} \equiv \binom{n \bmod p}{k \bmod p} \cdot \binom{\lfloor n/p \rfloor}{\lfloor k/p \rfloor} \pmod{p} (kn)≡(kmodpnmodp)⋅(⌊k/p⌋⌊n/p⌋)(modp)

cpp 复制代码
class Lucas {
public:
    long long C_small(int n, int k, long long p) {
        if (k < 0 || k > n) return 0;
        long long res = 1;
        for (int i = 1; i <= k; i++) {
            res = res * (n - k + i) % p;
            res = res * pow_mod(i, p - 2, p) % p;
        }
        return res;
    }

    long long lucas(long long n, long long k, long long p) {
        if (k == 0) return 1;
        return C_small(n % p, k % p, p) * lucas(n / p, k / p, p) % p;
    }

    long long pow_mod(long long a, long long b, long long m) {
        long long res = 1;
        while (b) {
            if (b & 1) res = res * a % m;
            a = a * a % m;
            b >>= 1;
        }
        return res;
    }
};
5.x.4.3 注意事项
  • 除法不能直接取模,必须转化为乘以逆元
  • 同余式两边不能随意约去公因子,除非公因子与模数互质
  • 逆元存在条件 : gcd ⁡ ( a , m ) = 1 \gcd(a, m) = 1 gcd(a,m)=1
  • 前缀和同余统计是竞赛中最常用的同余应用模式
  • 二分答案 + 容斥处理"第 k 个满足条件的数"问题
  • 同余最短路处理"用若干个数能组合出哪些数"的问题
  • 多组测试要清空哈希表和数组
  • 负数取模统一用 (a % m + m) % m 转成非负余数### 5.x.4 补充技巧:同余元理的应用
相关推荐
悠仁さん3 小时前
非类型模板参数的特化与偏特化
java·数据结构·算法
垆边人似月.3 小时前
最优河堤加固(二分答案 + 前缀和)
算法
深圳老胡3 小时前
C语言实现C++面向对象程序设计示例
笔记·stm32·单片机·代码规范
无忧.芙桃4 小时前
C++语言原理与实践(十三):stack、queue 和 priority_queue的介绍与使用
开发语言·c++·栈·队列
171320330计算机毕设编程4 小时前
计算机毕业设计 基于Hadoop的新能源汽车发展前景分析 LW PPT
java·大数据·hadoop·python·算法·随机森林·线性回归
垆边人似月.4 小时前
幻兽防御战(贪心算法)
数据结构·算法
DongQiShanRen4 小时前
六根硬件映射(下):物理闭环五步法,抽一条已实装的看
大数据·jvm·人工智能·算法·rust·哈希算法·集成学习
Code_Solitude4 小时前
c语言:指针(3)+冒泡排序
java·c语言·算法
yuehuaxin014 小时前
LS8011C 快充协议芯片|SOT‑23‑6L 内置 431,PD3.2 SPR/PPS 适配器物料备选评估调试笔记
笔记·智能家居·适配器模式·智能硬件
FPGA信号处理4 小时前
【模式识别】第四节课:支持向量机
算法·机器学习·支持向量机