文章目录
-
- 五、数学与数论
-
- [5.1 快速幂 / 逆元](#5.1 快速幂 / 逆元)
- [5.2 组合数预处理](#5.2 组合数预处理)
- [5.3 矩阵快速幂](#5.3 矩阵快速幂)
- [5.4 最大公约数 / 扩展欧几里得](#5.4 最大公约数 / 扩展欧几里得)
- [5.5 高斯消元法](#5.5 高斯消元法)
-
- 5.5.1普通版高斯消元
- [5.5.2 高斯消元异或版](#5.5.2 高斯消元异或版)
- [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。)
- 模版题链接:洛谷P4035模版题
思路分析:
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
- 模版题链接:洛谷P2962模版题
核心思路:
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 补充技巧:同余元理的应用