usingnamespace std; using ll = longlong; // 使用 ll 代替 long long
// 判断 n 是否为素数 (试除法) // 时间复杂度: O(sqrt(n)) boolis_prime(int n){ if (n < 2) returnfalse; // 注意 i*i 可能溢出 int,但这里 n 是 int,所以 i*i <= n <= INT_MAX,通常不会溢出 // 如果 n 是 ll 类型,则应写为 for (ll i = 2; i * i <= n; ++i) for (int i = 2; i * i <= n; ++i) { if (n % i == 0) { returnfalse; } } returntrue; }
intmain(){ ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); int n; cin >> n; if (is_prime(n)) { cout << n << " is prime\n"; } else { cout << n << " is not prime\n"; } return0; }
如何分解质因数? 同样使用试除法,从 2 开始尝试除 n,如果 i 能整除 n,就不断除以 i 并记录 i 的次数,直到 n 不能再被 i 整除。然后尝试下一个数 i+1。持续这个过程直到 i * i > n。如果此时 n 仍然大于 1,那么剩下的 n 本身也是一个素因子。
原理: 基于 gcd(a, b) = gcd(b, a % b) 的递归过程。 假设我们已经求得 b 和 a % b 的一组解 x' 和 y',满足: 我们知道 a % b = a - floor(a / b) * b,代入上式: 整理得: 由于 gcd(a, b) = gcd(b, a % b),我们令: 这就找到了满足 ax + by = gcd(a, b) 的一组解 (x, y)。递归的边界是当 b = 0 时,gcd(a, 0) = a,此时 ax + 0y = a,解为 x = 1, y = 0。
usingnamespace std; using ll = longlong; // 使用 ll 代替 long long
// 扩展欧几里得算法 // 求解 ax + by = gcd(a, b) 的一组解 (x, y) // 返回 gcd(a, b),并将解存储在 x, y 引用的变量中 // 时间复杂度: O(log(min(a, b))) ll exgcd(ll a, ll b, ll &x, ll &y){ if (b == 0) { x = 1; y = 0; return a; } ll d = exgcd(b, a % b, x, y); ll temp = x; x = y; y = temp - (a / b) * y; return d; }
intmain(){ ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); ll a, b, x, y; cin >> a >> b; ll d = exgcd(a, b, x, y); cout << "gcd(" << a << ", " << b << ") = " << d << "\n"; cout << "A solution for " << a << "x + " << b << "y = " << d << " is: "; cout << "x = " << x << ", y = " << y << "\n"; // 验证 cout << "Verification: " << a << "*" << x << " + " << b << "*" << y << " = " << a * x + b * y << "\n";
// 求解线性丢番图方程 ax + by = c ll c; cout << "Enter c for equation " << a << "x + " << b << "y = c: "; cin >> c; if (c % d != 0) { cout << "No integer solution exists.\n"; } else { // 找到 ax + by = d 的一组解 x0, y0 (即上面求出的 x, y) // 则 ax + by = c 的一组特解是 x1 = x0 * (c/d), y1 = y0 * (c/d) // 注意 x * (c / d) 可能溢出 ll,如果 a, b, c 很大 // 竞赛中通常假设不会爆 ll,或者题目会有说明 ll factor = c / d; ll x1 = x * factor; ll y1 = y * factor; cout << "One particular solution for " << a << "x + " << b << "y = " << c << " is: "; cout << "x = " << x1 << ", y = " << y1 << "\n";
// 通解形式: x = x1 + k * (b/d), y = y1 - k * (a/d) for integer k ll b_d = b / d; ll a_d = a / d; cout << "General solution form: x = " << x1 << " + k*(" << b_d << "), y = " << y1 << " - k*(" << a_d << ")\n"; // 求最小正整数解 x // k = (x1 % b_d - x1) / b_d; // 这样计算 k 不太稳健 // 最小非负 x 解为 (x1 % b_d + b_d) % b_d ll min_non_neg_x = (x1 % b_d + b_d) % b_d; // 如果要求最小正整数解 ll min_pos_x = min_non_neg_x; if (min_pos_x == 0) { min_pos_x += abs(b_d); // 加 b/d 的绝对值,因为 b/d 可能为负 } cout << "Smallest positive integer solution for x (if exists): " << min_pos_x << "\n"; }
return0; }
ExGCD 的主要应用:
求解线性丢番图方程 (Linear Diophantine Equation): 形如 ax + by = c 的方程。该方程有整数解当且仅当 gcd(a, b) | c。若有解,可用 ExGCD 求出 ax + by = gcd(a, b) 的一组解 (x_0, y_0),则 ax + by = c 的一组特解为 (x_1, y_1) = (x_0 * (c/d), y_0 * (c/d)),其中 d = gcd(a, b)。通解为 x = x_1 + k * (b/d), y = y_1 - k * (a/d),其中 k 为任意整数。
如果两个整数 a 和 b 除以一个正整数 m 所得的余数相同,则称 a 和 b模 m 同余。记作: 这等价于 m | (a - b)。m 称为**模数 (Modulus)**。
同余的性质 (模 m 下):
自反性:a ≡ a
对称性:若 a ≡ b,则 b ≡ a
传递性:若 a ≡ b 且 b ≡ c,则 a ≡ c
和差性: 若 a ≡ b 且 c ≡ d,则 a + c ≡ b + d 且 a - c ≡ b - d
积性: 若 a ≡ b 且 c ≡ d,则 ac ≡ bd
幂性: 若 a ≡ b,则 a^k ≡ b^k (k 为非负整数)
若 ac ≡ bc \pmod m 且 gcd(c, m) = d,则 a ≡ b \pmod{m/d}。特别地,若 gcd(c, m) = 1,则 a ≡ b \pmod m (模意义下的除法需要小心)。
这些性质意味着我们可以在运算过程中随时取模,而不影响最终模 m 的结果(除法除外)。这对于防止中间结果溢出至关重要。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
// C++ 中的取模运算注意事项 #include<iostream> using ll = longlong; // 使用 ll 代替 long long
intmain(){ ll a = -5; ll m = 3; // C++ 的 % 运算符对于负数的结果可能也是负数 (-5 % 3 = -2) ll cpp_remainder = a % m; std::cout << "C++ %: " << cpp_remainder << std::endl; // Output: -2 (或依赖编译器)
// 在数论中,我们通常希望余数是非负的 (介于 0 和 m-1 之间) ll positive_remainder = (a % m + m) % m; // 结果为 1 std::cout << "Positive remainder: " << positive_remainder << std::endl; // Output: 1 return0; }
2. 模逆元 (Modular Inverse)
在实数运算中,a 的逆元是 1/a,因为 a * (1/a) = 1。模运算中也有类似的概念。 如果存在一个整数 x 使得: 则称 x 是 a模 m 的乘法逆元。通常记作 。
模逆元存在的条件:a 模 m 的逆元存在 当且仅当gcd(a, m) = 1。即 a 和 m 互素。
为什么需要模逆元? 模运算对于加、减、乘是封闭的,但没有直接的“模除法”。如果你想计算 (a / b) % m,你不能简单地计算 (a % m) / (b % m) % m。正确的方法是找到 b 模 m 的逆元 b^{-1},然后计算: 这在处理组合计数问题(如计算组合数 )时非常常见,因为组合数公式涉及除法。
如何求解模逆元?
方法一:使用扩展欧几里得算法 (ExGCD) 求解 ax \equiv 1 \pmod m 等价于求解线性丢番图方程 ax + my = 1。 根据 ExGCD,这个方程有解当且仅当 gcd(a, m) | 1,即 gcd(a, m) = 1。 当 gcd(a, m) = 1 时,我们使用 ExGCD 找到 x, y 使得 ax + my = 1。那么这个 x 就是 a 模 m 的一个逆元。因为我们通常需要一个 [0, m-1] 范围内的逆元,所以最终结果是 (x % m + m) % m。
usingnamespace std; using ll = longlong; // 使用 ll 代替 long long
// ExGCD (复用之前的代码) ll exgcd(ll a, ll b, ll &x, ll &y){ if (b == 0) { x = 1; y = 0; return a; } ll d = exgcd(b, a % b, x, y); ll temp = x; x = y; y = temp - (a / b) * y; return d; }
// 求解 a 模 m 的逆元 // 返回 -1 表示逆元不存在 // 时间复杂度: O(log(min(a, m))) ll modInverse_exgcd(ll a, ll m){ ll x, y; ll d = exgcd(a, m, x, y); if (d != 1) { return-1; // 逆元不存在 } // 确保逆元是正数且在 [0, m-1] 范围内 return (x % m + m) % m; }
intmain(){ ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); ll a = 7; ll m = 26; // gcd(7, 26) = 1 ll inv = modInverse_exgcd(a, m); if (inv != -1) { cout << "Inverse of " << a << " mod " << m << " is " << inv << "\n"; // inv = 15 cout << "Verification: (" << a << " * " << inv << ") % " << m << " = " << (a * inv) % m << "\n"; // 7 * 15 = 105. 105 % 26 = 1. } else { cout << "Inverse of " << a << " mod " << m << " does not exist.\n"; }
a = 6; m = 26; // gcd(6, 26) = 2 inv = modInverse_exgcd(a, m); if (inv != -1) { cout << "Inverse of " << a << " mod " << m << " is " << inv << "\n"; } else { cout << "Inverse of " << a << " mod " << m << " does not exist.\n"; // Correct } return0; }
方法二:费马小定理 (Fermat’s Little Theorem) 条件: 模数 m必须是素数,并且 a 不能是 m 的倍数 (a % m != 0)。 定理内容: 若 p 是素数,且 p 不能整除 a,则: 推论求逆元: 将上式两边同乘以 (假设存在),得到: 所以,当 p 是素数时,a 模 p 的逆元就是 。我们可以使用快速幂 (Modular Exponentiation) 算法来高效计算 。
快速幂算法 (Modular Exponentiation / Exponentiation by Squaring) 用于快速计算 。其思想是将指数 b 表示为二进制形式,然后利用 和 来加速计算。
usingnamespace std; using ll = longlong; // 使用 ll 代替 long long
// 快速幂算法 计算 (base^exp) % mod // 时间复杂度: O(log(exp)) ll qpow(ll base, ll exp, ll mod){ ll res = 1; base %= mod; // 预处理,防止 base 过大或为负 if (base < 0) base += mod; // 确保 base 非负 while (exp > 0) { if (exp % 2 == 1) { // 如果当前位是 1 res = (res * base) % mod; } base = (base * base) % mod; // base 平方 exp /= 2; // 指数右移一位 } return res; }
// 使用费马小定理求逆元 (要求 mod 是素数) // 时间复杂度: O(log(mod)) ll modInverse_fermat(ll a, ll p){ if (p <= 1) return-1; // 素数必须大于 1 // 这里可以加一个素性测试,但通常题目会保证 p 是素数 a %= p; if (a < 0) a += p; if (a == 0) return-1; // a 是 p 的倍数,无逆元 // gcd(a, p) != 1 时无逆元,这已包含在 a=0 的情况里,因为 p 是素数
// 指数是 p - 2 if (p - 2 < 0) { // 处理 p=2 的情况 if (p == 2 && a == 1) return1; // 1 mod 2 的逆元是 1 elsereturn-1; // 其他情况 p=2 时指数为 0 或负数,或 a=0 mod 2 } returnqpow(a, p - 2, p); }
intmain(){ ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); ll a = 7; ll p = 29; // 29 is prime ll inv = modInverse_fermat(a, p); if (inv != -1) { cout << "Inverse of " << a << " mod " << p << " is " << inv << "\n"; // inv = 25 cout << "Verification: (" << a << " * " << inv << ") % " << p << " = " << (a * inv) % p << "\n"; // 7 * 25 = 175. 175 = 6*29 + 1. 175 % 29 = 1. } else { cout << "Inverse of " << a << " mod " << p << " does not exist or p is not prime.\n"; }
a = 3; p = 2; // 2 is prime inv = modInverse_fermat(a, p); // a=3 % 2 = 1. p-2=0. 1^0 = 1. if (inv != -1) { cout << "Inverse of " << a << " mod " << p << " is " << inv << "\n"; // inv = 1 cout << "Verification: (" << a << " * " << inv << ") % " << p << " = " << (a * inv) % p << "\n"; // (3*1)%2 = 1 } else { cout << "Inverse of " << a << " mod " << p << " does not exist or p is not prime.\n"; }
return0; }
ExGCD vs. 费马小定理:
ExGCD:
优点:适用范围更广,只要 gcd(a, m) = 1 即可,m 不必是素数。
缺点:实现稍复杂一点点。
费马小定理:
优点:实现简单,只需要快速幂。
缺点:强烈依赖 m 是素数 这个条件。如果 m 不是素数,用费马小定理求逆元是错误的!
竞赛中如何选择? 如果题目明确说明模数 p 是一个大素数 (常见的如 10^9 + 7, 998244353),并且你需要求逆元,使用费马小定理 + 快速幂通常更方便。 如果模数 m 不确定是否为素数,或者可能不是素数,必须使用 ExGCD 来求解逆元。
方法三:线性递推求逆元 (Linear Method for Inverses) 有时我们需要预处理出 1 到 n 所有数模 p (p 通常是素数) 的逆元。如果对每个数都用 ExGCD 或快速幂,复杂度是 O(n log p)。存在一种 O(n) 的线性递推方法。
令 inv[i] 表示 i 模 p 的逆元。我们要求 inv[i]。 设 p = k * i + r,其中 (这是带余除法的定义)。 则 k = floor(p / i),r = p % i。 将上式置于模 p 意义下: 两边同乘以 (假设它们都存在): 将 k 和 r 的表达式代回: 由于 p % i < i,inv[p % i] 在计算 inv[i] 之前已经被计算出来了。这就是递推关系。 边界是 inv[1] = 1。
// 埃氏筛法,找出 [2, n] 范围内的所有素数 // 时间复杂度: O(n log log n) voidsieve_eratosthenes(int n){ if (n >= MAXN) { /* Handle error or resize */return; } fill(is_composite, is_composite + n + 1, false); is_composite[0] = is_composite[1] = true; // 注意 p*p 溢出 int 的问题 for (int p = 2; (ll)p * p <= n; ++p) { // 强制转 ll 判断 if (!is_composite[p]) { // p 是素数 // 将 p 的倍数标记为合数 // 从 p*p 开始标记,因为小于 p*p 的合数已被更小的素数标记过 // 注意 i 溢出 int 的问题 for (ll i = (ll)p * p; i <= n; i += p) { // 使用 ll 迭代 if (i < MAXN) { // 检查数组越界 is_composite[i] = true; } else { // 如果 i 超过了数组大小,理论上不应发生,因为 i<=n 且 n < MAXN break; } } } } // 收集素数 primes.clear(); for (int p = 2; p <= n; ++p) { if (!is_composite[p]) { primes.push_back(p); } } }
intmain(){ ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); int n = 100; sieve_eratosthenes(n); cout << "Primes up to " << n << ":\n"; for (int p : primes) { cout << p << " "; } cout << "\n"; return0; }
复杂度分析: 埃氏筛法的复杂度是 ,非常接近线性。直观理解,每个数 k 被其素因子标记。对于素数 p,它会标记 N/p 个数。所有素数加起来,。
如果 i 没有被标记过(即 is_composite[i] 为 false),说明 i 是素数,将其加入 primes 列表。
遍历已找到的素数 p (从 primes[0] 开始):
将 i * p 标记为合数 (is_composite[i * p] = true)。
关键一步: 如果 i 是 p 的倍数 (i % p == 0),则停止用当前 i 去筛后续的素数。
原因: 如果 i % p == 0,说明 p 是 i 的最小素因子(因为 p 是从 primes 列表里从小到大取的)。那么对于任何比 p 更大的素数 p' (即 primes 列表中 p 后面的素数),i * p' 这个合数的最小素因子仍然是 p (因为 i 包含因子 p),而不是 p'。根据线性筛的原则(每个合数只被其最小素因子筛掉),i * p' 应该在后面遍历到 (i * p' / p) 这个数时,被 p 筛掉,而不是现在被 p' 筛掉。因此,当 i % p == 0 时,我们必须 break,停止用当前的 i 和更大的素数 p' 去筛,以保证每个合数只被其最小素因子筛一次。
// 关键: 保证每个合数只被其最小素因子筛一次 if (i % pj == 0) { // 如果 i 能被 pj 整除, 说明 pj 是 i 的最小素因子 // 那么 i * primes[k] (k>j) 的最小素因子也是 pj // 这些数应该在后面 i' = i * primes[k] / pj 时被 pj 筛掉 // 所以这里 break, 不再用 i 去筛更大的素数 break; } } } }
intmain(){ ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); int n = 100; sieve_linear(n); cout << "Primes up to " << n << " using linear sieve:\n"; for (int p : primes) { cout << p << " "; } cout << "\n";
// Optional: Print minimum prime factors // cout << "\nMinimum prime factor for numbers up to " << n << ":\n"; // for (int i = 2; i <= n; ++i) { // cout << "min_prime_factor(" << i << ") = " << min_prime_factor[i] << "\n"; // }
什么是积性函数? 一个数论函数 f(n),如果对于任意一对互素的正整数 a 和 b (即 gcd(a, b) = 1),都有 f(a * b) = f(a) * f(b),则称 f 为积性函数。 如果对于任意正整数 a 和 b,都有 f(a * b) = f(a) * f(b),则称 f 为完全积性函数。
线性筛利用 n = i * p (其中 p 是 n 的最小素因子) 这个关系,结合积性函数的性质,可以在 O(1) 的时间内由 f(i) 和 f(p)(或相关值)计算出 f(n)。
usingnamespace std; using ll = longlong; // 使用 ll 代替 long long
// 计算单个 n 的欧拉函数 phi(n) // 时间复杂度: O(sqrt(n)) ll phi(ll n){ if (n <= 0) return0; // 处理非正整数输入 ll res = n; // 使用 ll 迭代 for (ll i = 2; i * i <= n; ++i) { if (n % i == 0) { res = res / i * (i - 1); // 等价于 res = res * (1 - 1/i) while (n % i == 0) { n /= i; } } } if (n > 1) { // 如果 n 还有一个大于 sqrt(n) 的质因子 res = res / n * (n - 1); } return res; }
for (int i = 2; i <= n; ++i) { if (!is_composite[i]) { primes.push_back(i); phi_val[i] = i - 1; // p 是素数, phi(p) = p - 1 } for (int pj : primes) { if (1ll * i * pj > n) break; // 注意溢出 is_composite[i * pj] = true; if (i % pj == 0) { // pj 是 i 的一个素因子 (也是 i*pj 的最小素因子) // phi(i*pj) = phi(i) * pj phi_val[i * pj] = phi_val[i] * pj; // pj 是 int, phi_val[i] 是 ll break; // 保证线性时间 } else { // i 和 pj 互素 // phi(i*pj) = phi(i) * phi(pj) = phi(i) * (pj - 1) phi_val[i * pj] = phi_val[i] * (pj - 1); } } } }
intmain(){ ios::sync_with_stdio(0); cin.tie(0); cout.tie(0); int n = 20; sieve_phi(n); cout << "Euler's totient function phi(k) for k from 1 to " << n << ":\n"; for (int i = 1; i <= n; ++i) { cout << "phi(" << i << ") = " << phi_val[i] << "\n"; } return0; }
2. 欧拉定理 (Euler’s Theorem)
这是费马小定理的推广。
定理内容: 若 a 和 m 是互素的正整数 (即 gcd(a, m) = 1),则:
证明思路: 考虑所有小于 m 且与 m 互素的 个数 。因为 gcd(a, m) = 1,所以集合 恰好是集合 的一个排列。将两组数分别相乘并模 m,得到: 令 。因为所有 都与 m 互素,所以它们的乘积 R 也与 m 互素,即 gcd(R, m) = 1。根据同余的性质,我们可以约掉 R:
应用:
降幂: 在计算 时,如果 gcd(a, m) = 1,我们可以将指数 b 对 取模,即: 注意: 这个降幂公式严格来说要求 。对于 ,直接计算 。 扩展欧拉定理 (Extended Euler’s Theorem): 处理 gcd(a, m) != 1 或 b < phi(m) 的情况。 这个形式可以统一处理所有情况(即使 gcd(a, m) != 1)。在竞赛中,如果你需要计算 ,可以直接用扩展欧拉定理的公式来降幂:先计算 ,然后令新的指数为 。但是,需要判断原始的 是否小于 ,如果 ,则指数应该直接是 。
#include<bits/stdc++.h> usingnamespace std; using ll = longlong; // 使用 ll 代替 long long
// 快速幂 (复用) ll qpow(ll base, ll exp, ll mod){ ll res = 1; base %= mod; if(base < 0) base += mod; while (exp > 0) { if (exp % 2 == 1) res = (res * base) % mod; base = (base * base) % mod; exp /= 2; } return res; }
// 计算单个 phi(n) (复用) ll phi(ll n){ if (n <= 0) return0; ll res = n; for (ll i = 2; i * i <= n; ++i) { if (n % i == 0) { res = res / i * (i - 1); while (n % i == 0) n /= i; } } if (n > 1) res = res / n * (n - 1); return res; }
// 使用扩展欧拉定理计算 a^b mod m // 注意: b 可能是非常大的数 ll power_extended_euler(ll a, ll b, ll m){ if (m == 1) return0; // 对 1 取模结果总是 0 if (b == 0) return1 % m; // a^0 = 1
usingnamespace std; using ll = longlong; // 使用 ll 代替 long long
// ExGCD (复用) ll exgcd(ll a, ll b, ll &x, ll &y){ if (b == 0) { x = 1; y = 0; return a; } ll d = exgcd(b, a % b, x, y); ll temp = x; x = y; y = temp - (a / b) * y; return d; }
// 模逆元 (复用 ExGCD) ll modInverse(ll a, ll m){ ll x, y; ll d = exgcd(a, m, x, y); if (d != 1) return-1; // 逆元不存在 return (x % m + m) % m; }
// 安全乘法取模,防止 a*b 溢出 ll ll safe_mul(ll a, ll b, ll m){ a %= m; b %= m; ll res = 0; while (b > 0) { if (b & 1) res = (res + a) % m; a = (a + a) % m; b >>= 1; } return res; // 或者如果 __int128 可用: return (ll)((__int128)a * b % m); }
// 中国剩余定理 (CRT) // 求解 x = a_i (mod m_i), 其中 m_i 两两互素 // a: 存储余数的数组 (vector<ll>) // m: 存储模数的数组 (vector<ll>) // 返回最小非负整数解 x 和总模数 M // 时间复杂度: O(k * log(M)), k 是方程个数, M 是模数乘积 pair<ll, ll> crt(const vector<ll>& a, const vector<ll>& m){ int k = a.size(); if (k == 0) return {0, 1};
ll M = 1; // 所有模数的乘积 for (ll mi : m) { // 需要检查 M * mi 是否溢出 ll if ((double)M * mi > 2e18) { // 粗略检查溢出,更精确需要 __int128 // Handle overflow error, maybe return {-2, -2} // 在竞赛中,有时 M 会爆 ll,题目可能要求对另一个大数取模,或者保证 M 不爆 // 这里假设 M 不会爆 ll } M *= mi; } if (M == 0) { // 如果某个 mi 是 0,处理一下,这里简单返回错误 return {-1, -1}; }
ll x0 = 0; for (int i = 0; i < k; ++i) { if (m[i] == 0) return {-1, -1}; // 模数不能为 0 ll Mi = M / m[i]; ll ti = modInverse(Mi, m[i]); if (ti == -1) { // m_i 不互素时会发生,但 CRT 假设互素 return {-1, -1}; // 表示无解或输入错误 }
// 计算项 (a[i] * Mi * ti) % M // 需要使用安全乘法防止溢出 ll term = safe_mul(a[i], Mi, M); term = safe_mul(term, ti, M); // 如果用 __int128: // __int128 term128 = (__int128)a[i] * Mi % M * ti % M; // x0 = (x0 + (ll)term128) % M;
x0 = (x0 + term) % M; }
// 结果可能是负数,调整为最小非负解 return {(x0 + M) % M, M}; // 返回解 x0 和总模数 M }
usingnamespace std; using ll = longlong; // 使用 ll 代替 long long
// ExGCD (复用) ll exgcd(ll a, ll b, ll &x, ll &y){ if (b == 0) { x = 1; y = 0; return a; } ll d = exgcd(b, a % b, x, y); ll temp = x; x = y; y = temp - (a / b) * y; return d; }
// GCD (复用) ll gcd(ll a, ll b){ return b == 0 ? abs(a) : gcd(b, a % b); } // 处理负数,返回非负
// 最小公倍数 (复用 gcd) ll lcm(ll a, ll b){ if (a == 0 || b == 0) return0; ll common_divisor = gcd(a, b); // 防止溢出: (a / gcd) * b ll res_num = a / common_divisor; // 检查 res_num * b 是否溢出 if (abs(b) > 0 && abs(res_num) > numeric_limits<ll>::max() / abs(b)) { return-2; // 返回特殊值表示溢出 } return res_num * b; }
// 安全乘法取模 (复用) ll safe_mul(ll a, ll b, ll m){ a %= m; b %= m; if (a < 0) a += m; if (b < 0) b += m; ll res = 0; while (b > 0) { if (b & 1) res = (res + a); if (res >= m) res -=m; // 手动取模防止 res+a 溢出一点点 a = (a + a); if (a >= m) a -= m; b >>= 1; } return res; // Or use __int128 if available and needed // return (ll)((__int128)a * b % m); }
// 扩展中国剩余定理 (Excrt) // 求解 x = a_i (mod m_i), m_i 不一定两两互素 // 返回 pair<ll, ll>: {最小非负解 x, 最终模数 M = lcm(m_i)} // 如果无解,返回 {-1, -1} // 如果 LCM 计算溢出,返回 {-2, -2} pair<ll, ll> excrt(const vector<ll>& a, const vector<ll>& m){ int k = a.size(); if (k == 0) return {0, 1};
ll ans = a[0]; // 初始解满足第一个方程 ll M = m[0]; // 初始模数 if (M <= 0) return {-1, -1}; // 模数必须为正
ans = (ans % M + M) % M; // 保证初始解非负
for (int i = 1; i < k; ++i) { ll mi = m[i]; ll ai = a[i]; if (mi <= 0) return {-1, -1}; // 模数必须为正
// 合并方程 x = ans (mod M) 和 x = ai (mod mi) // 求解 kM = ai - ans (mod mi) ll A = M; ll B = mi; ll C = (ai - ans % B + B) % B; // C = (ai - ans) mod B, 保证 C 非负
ll x, y; ll d = exgcd(A, B, x, y); // d = gcd(M, mi)
if (C % d != 0) { return {-1, -1}; // 方程组无解 }
// 求解 k = x * (C / d) (mod B / d) ll mod_k = B / d; // x * (C / d) 可能溢出,需要安全乘法 // ll k0 = safe_mul(x, C / d, mod_k); // 使用 __int128 通常更方便且不易出错(如果允许) ll k0 = (ll)((__int128)x * (C / d) % mod_k);
k0 = (k0 + mod_k) % mod_k; // 确保 k0 非负
// 更新解和模数 // ans = ans + k0 * M; // 可能溢出 // M_new = lcm(M, mi); // 可能溢出
usingnamespace std; using ll = longlong; // 使用 ll 代替 long long
constint MAXN = 1000005; ll p = 1e9 + 7; // 假设 p 是素数 ll fact[MAXN]; ll invfact[MAXN];
// 快速幂 (复用) ll qpow(ll base, ll exp, ll mod){ ll res = 1; base %= mod; if(base < 0) base += mod; while (exp > 0) { if (exp % 2 == 1) res = (res * base) % mod; base = (base * base) % mod; exp /= 2; } return res; }
// 预处理阶乘和阶乘逆元 // 时间复杂度: O(N + log p) (用快速幂求第一个逆元) 或 O(N) (如果 p 是素数) voidprecompute_combinations(int n){ if (n >= MAXN) { /* Handle error or resize */return; } fact[0] = 1; invfact[0] = 1; for (int i = 1; i <= n; ++i) { fact[i] = (fact[i - 1] * i) % p; } // 计算 fact[n] 的逆元 // 如果 p 不是素数,这里必须用 ExGCD 求逆元 // invfact[n] = modInverse_exgcd(fact[n], p); invfact[n] = qpow(fact[n], p - 2, p); // 使用费马小定理 (仅当 p 是素数) if (invfact[n] == -1 && fact[n] != 0) { /* Handle error: inverse doesn't exist */return;}
// 递推计算其他阶乘逆元 for (int i = n - 1; i >= 1; --i) { // invfact[i] = invfact[i+1] * (i+1) mod p // 注意 (i+1) 可能大于 p,所以要取模 invfact[i] = (invfact[i + 1] * ((i + 1) % p)) % p; } }
// 计算 C(n, k) mod p // 时间复杂度: O(1) (在预处理之后) ll combinations(int n, int k){ if (k < 0 || k > n || n >= MAXN) { // 检查 k 范围和 n 是否超出预处理范围 return0; } // C(n, k) = n! / (k! * (n-k)!) mod p // C(n, k) = fact[n] * invfact[k] * invfact[n-k] mod p ll res = fact[n]; res = (res * invfact[k]) % p; res = (res * invfact[n - k]) % p; return (res % p + p) % p; // 确保结果非负 }
usingnamespace std; using ll = longlong; // 使用 ll 代替 long long
// --- 需要组合数计算 C(n, k) mod p (n, k < p) --- constint MAXP = 1005; // 假设 p 不会很大,预处理阶乘到 p-1 ll fact_lucas[MAXP]; ll invfact_lucas[MAXP]; ll current_p = -1; // 当前 Lucas 定理使用的素数模数
// 快速幂 (复用) ll qpow(ll base, ll exp, ll mod){ ll res = 1; base %= mod; if(base < 0) base += mod; while (exp > 0) { if (exp % 2 == 1) res = (res * base) % mod; base = (base * base) % mod; exp /= 2; } return res; } // 模逆元 (复用费马小定理) ll modInverse_fermat(ll a, ll p){ if (p <= 1) return-1; a %= p; if(a<0) a+=p; if (a == 0) return-1; if (p - 2 < 0) return (p == 2 && a == 1) ? 1 : -1; returnqpow(a, p - 2, p); }
// 预处理 Lucas 定理所需的阶乘和逆元 (模 p) voidprecompute_lucas(ll p){ if (p == current_p) return; // 如果模数没变,无需重新计算 if (p >= MAXP) { /* Handle error: p is too large for precomputation array */return; } current_p = p; fact_lucas[0] = 1; invfact_lucas[0] = 1; for (int i = 1; i < p; ++i) { fact_lucas[i] = (fact_lucas[i - 1] * i) % p; } invfact_lucas[p - 1] = modInverse_fermat(fact_lucas[p - 1], p); if (invfact_lucas[p - 1] == -1 && fact_lucas[p-1] != 0) { /* Error */return; } for (int i = p - 2; i >= 1; --i) { invfact_lucas[i] = (invfact_lucas[i + 1] * ((i + 1) % p)) % p; } }
// 计算 C(n, k) mod p (n, k < p) ll combinations_small(ll n, ll k, ll p){ if (k < 0 || k > n) return0; if (k == 0 || k == n) return1; if (k > n / 2) k = n - k; // 优化 C(n, k) = C(n, n-k) if (n >= p || k >= p || p != current_p) { // 如果 n, k >= p 或模数变化,需要报错或重新预处理 if (p != current_p) precompute_lucas(p); // 尝试重新预处理 if (n >= p || k >= p || p!= current_p) return-1; // 预处理失败或参数错误 } // 使用预处理结果 ll res = fact_lucas[n]; res = (res * invfact_lucas[k]) % p; res = (res * invfact_lucas[n - k]) % p; return (res % p + p) % p; }
// 卢卡斯定理 计算 C(n, k) mod p (p 是素数) // 时间复杂度: O(p + log_p(n) * log p) (预处理O(p), 每次递归O(log p)求组合数, 递归log_p(n)次) // 如果组合数查询是 O(1), 则复杂度是 O(p + log_p(n)) ll lucas(ll n, ll k, ll p){ if (k < 0 || k > n) return0; if (k == 0) return1; // 确保预处理完成 if (p != current_p) { precompute_lucas(p); if (p != current_p) return-1; // 预处理失败 }
ll ni = n % p; ll ki = k % p;
// 计算 C(ni, ki) mod p ll c_nk = combinations_small(ni, ki, p); if (c_nk == 0) return0; // 优化:如果某一项是 0,总结果就是 0
usingnamespace std; using ll = longlong; // 使用 ll 代替 long long
// --- 需要 ExGCD, qpow, safe_mul, CRT ---
// ExGCD (复用) ll exgcd(ll a, ll b, ll &x, ll &y){ if (b == 0) { x = 1; y = 0; return a; } ll d = exgcd(b, a % b, x, y); ll temp = x; x = y; y = temp - (a / b) * y; return d; }
// 模逆元 (复用 ExGCD) ll modInverse(ll a, ll m){ ll x, y; ll d = exgcd(a, m, x, y); if (d != 1) return-1; // 逆元不存在 return (x % m + m) % m; }
// 快速幂 (复用) ll qpow(ll base, ll exp, ll mod){ ll res = 1; base %= mod; if(base < 0) base += mod; while (exp > 0) { if (exp % 2 == 1) res = (res * base) % mod; base = (base * base) % mod; exp /= 2; } return res; }
// 计算 n! 中因子 p 的个数 (勒让德定理) ll count_prime_factors(ll n, ll p){ ll count = 0; while (n > 0) { count += n / p; n /= p; } return count; }
// 计算 n! / p^v mod p^a,即 R_n // 其中 v = count_prime_factors(n, p) // p 是素数, pe = p^a ll factorial_mod_pk(ll n, ll p, ll pe){ if (n == 0) return1; ll res = 1; // 计算 [1, pe] 中不含因子 p 的数的乘积模 pe for (ll i = 1; i <= pe; ++i) { if (i % p != 0) { res = (res * i) % pe; } } // [1, pe] 的结果可以被快速幂应用到 n / pe 次 res = qpow(res, n / pe, pe);
// 计算剩余部分 [1, n % pe] 中不含因子 p 的数的乘积 for (ll i = 1; i <= n % pe; ++i) { if (i % p != 0) { res = (res * i) % pe; } } // 递归计算 (n/p)! / p^v' mod pe res = (res * factorial_mod_pk(n / p, p, pe)) % pe; return res; }
// 计算 C(n, k) mod p^a (pe = p^a) ll combinations_mod_pk(ll n, ll k, ll p, ll pe){ if (k < 0 || k > n) return0; if (k == 0 || k == n) return1;