资讯动态

从Lucas定理到中国剩余定理:解析大组合数取模与欧拉降幂的经典应用

发布时间:2026/8/23 5:24:44 来源:尧图企业网站定制
1. 项目概述一道融合数论精华的经典竞赛题看到这个标题老竞赛党们DNA估计要动了。“古代猪文”这个听起来有点无厘头的名字背后其实是信息学奥赛OI和算法竞赛圈子里一道相当经典的题目来自洛谷Luogu题库的P2480原题出自SDOI2010。这道题之所以让人印象深刻甚至有点“谈虎色变”是因为它完美地融合了数论中几个核心且有一定难度的知识点组合数取模、Lucas定理以及最终的中国剩余定理CRT与欧拉定理的应用。它不像单纯的动态规划或者图论题有比较固定的套路可循而是需要你真正理解这些数论工具的原理并像搭积木一样将它们灵活地组合起来解决一个复杂问题。简单来说题目会给你两个巨大的整数N和G要求你计算 G^{sum} mod 999911659 的值。这里的 sum 是什么是所有满足 d 能整除 N 的 C(N, d) 之和。也就是说你需要计算 N 的所有正因数的组合数之和。N 可以非常大最大到 10^9G 也可能很大。直接计算组合数再求和是绝对不可能的更别提最后还有一个大指数的模幂运算。这道题的价值在于它提供了一个绝佳的“战场”让你系统性地演练如何解决“大组合数取模”和“大指数模运算”这两个在密码学、计算数学等领域非常实际的问题。如果你能独立啃下这道题那么你对数论的理解和应用能力会上一个大台阶。接下来我就带你一步步拆解这道题不仅告诉你“怎么做”更重点讲清楚“为什么这么做”以及我在反复提交和调试中积累的那些“血泪教训”。2. 核心思路拆解化整为零分而治之面对这种复合型难题最忌讳的就是一头扎进去蛮干。我们必须先站在高处看清整个问题的结构然后制定一个清晰的作战计划。整个计算过程可以分解为三个核心阶段它们环环相扣2.1 第一阶段目标转换与欧拉定理降幂我们的终极目标是计算G^sum mod MOD其中MOD 999911659而sum Σ C(N, d) (d|N)。第一步就要注意到一个关键性质999911659 是一个质数。这是题目给出的重要条件也是我们所有后续操作的基石。对于一个质数模数p我们有费马小定理欧拉定理在质数下的特例若G不是p的倍数则G^(p-1) ≡ 1 (mod p)。这里就遇到了第一个陷阱如果G是p的倍数即G % MOD 0那么结果直接就是0因为0的任何正数次幂对MOD取模都是0。这是一个需要特判的边界情况。如果G不是p的倍数我们就可以利用欧拉定理进行降幂。因为φ(p) p-1所以对于指数sum我们可以将其对p-1取模G^sum ≡ G^(sum mod (p-1)) (mod p)这样我们就把一个可能巨大的指数sum的问题转化为了计算sum mod (p-1)的问题。而p-1 999911658。注意新的模数p-1不再是质数了这为下一步埋下了伏笔。关键理解为什么模p-1这是欧拉定理的直接推论a^φ(n) ≡ 1 (mod n)当gcd(a, n)1。这里np是质数φ(p)p-1。所以a^k ≡ a^(k mod φ(p)) (mod p)。降幂是处理大指数模运算的标准操作。2.2 第二阶段分解模数与Lucas定理登场现在问题转化为计算sum_mod Σ C(N, d) mod (p-1)其中p-1 999911658。 直接计算C(N, d)对999911658取模依然极其困难因为模数不是质数我们无法使用求逆元等简便方法。这里就是第二个关键技巧分解模数。我们尝试分解999911658999911658 2 * 3 * 4679 * 35617我们发现它恰好是四个互质的质数的乘积。这简直是天赐良机它让我们可以运用中国剩余定理CRT。中国剩余定理告诉我们要计算X mod MM m1*m2*...*mk且两两互质等价于分别计算X mod m1,X mod m2, ...,X mod mk得到一组同余方程然后再用CRT合并回X mod M。因此我们只需要分别计算a1 sum mod 2a2 sum mod 3a3 sum mod 4679a4 sum mod 35617然后解同余方程组x ≡ a1 (mod 2) x ≡ a2 (mod 3) x ≡ a3 (mod 4679) x ≡ a4 (mod 35617)解出的x在模M999911658意义下就是我们要的sum_mod。那么如何计算sum mod mi呢这里mi是质数2, 3, 4679, 35617。对于质数模数下的组合数计算Lucas定理就派上用场了。Lucas定理对于质数p有C(n, m) mod p C(n mod p, m mod p) * C(n/p, m/p) mod p。 这是一个递归过程它将大规模的组合数计算分解为对p以下小数字的组合数计算极大地简化了问题。因为n mod p和m mod p都小于p我们可以预处理出0!到(p-1)!的阶乘和阶乘逆元然后O(1)地计算小组合数C(n%p, m%p)。所以对于每一个质因子mi我们预处理该模数下的阶乘数组fac[0..mi-1]和阶乘逆元数组invfac[0..mi-1]。枚举N的所有因数d。对于每个d使用 Lucas 定理计算C(N, d) mod mi。将所有C(N, d) mod mi相加并对mi取模得到ai。2.3 第三阶段中国剩余定理合并与最终计算得到四个同余方程的余数a1, a2, a3, a4后我们使用中国剩余定理求解x。 设M 2*3*4679*35617 999911658Mi M / mi。 我们需要找到ti使得Mi * ti ≡ 1 (mod mi)即ti是Mi在模mi意义下的逆元。因为mi是质数且Mi与mi互质这个逆元一定存在可以用扩展欧几里得算法或快速幂费马小定理求出。那么方程组的解为x ≡ Σ(ai * Mi * ti) (mod M)。 计算出的x就是sum_mod sum mod 999911658。最后我们回到第一阶段如果G % MOD 0输出0。否则计算ans fast_pow(G, sum_mod, MOD)即快速幂取模得到最终答案。整个算法的流程图如下逻辑描述输入 N, G MOD 999911659 if G % MOD 0: print(0); return M MOD - 1 999911658 分解 M 为质因子列表 primes [2, 3, 4679, 35617] 初始化余数数组 a[] for each p in primes: sum_p 0 预处理模 p 下的阶乘和逆元阶乘 for each divisor d of N: sum_p (sum_p lucas(N, d, p)) % p a[p] sum_p 使用中国剩余定理(CRT)根据 a[] 和 primes[] 求解 sum_mod ans fast_pow(G, sum_mod, MOD) 输出 ans3. 关键技术细节与实现要点思路清晰了但魔鬼藏在细节里。每个步骤的实现都有需要注意的坑点。3.1 质因数分解与因数枚举质因数分解题目给定的MOD-1是固定的所以我们直接硬编码分解结果[2, 3, 4679, 35617]即可不需要写通用的分解函数。因数枚举这是性能的关键点之一。N 最大为 1e9其因数个数不会太多1e9以内因数最多的数其因数个数大约在 1300 多个。我们可以用O(sqrt(N))的方法枚举所有因数。vectorlong long divisors; for (long long i 1; i * i N; i) { if (N % i 0) { divisors.push_back(i); if (i ! N / i) { // 避免重复添加平方根 divisors.push_back(N / i); } } }这样得到的divisors数组包含了N的所有正因数。3.2 Lucas定理的高效实现Lucas定理的递归实现非常简洁long long lucas(long long n, long long m, long long p) { if (m 0) return 1; // C(n, m) % p C(n%p, m%p) * lucas(n/p, m/p, p) % p return (C(n % p, m % p, p) * lucas(n / p, m / p, p)) % p; }其中C(n, m, p)函数用于计算当n, m p时的组合数取模。这要求我们提前预处理模p下的阶乘数组fac和阶乘逆元数组invfac。预处理阶乘与逆元fac[0] 1; for (int i 1; i p; i) { fac[i] fac[i-1] * i % p; } // 计算阶乘逆元利用费马小定理invfac[i] (i!)^(p-2) mod p invfac[p-1] fast_pow(fac[p-1], p-2, p); for (int i p-2; i 0; --i) { invfac[i] invfac[i1] * (i1) % p; }计算小组合数long long C(long long n, long long m, long long p) { if (n m) return 0; // 组合数定义n必须大于等于m // C(n, m) n! / (m! * (n-m)!) return fac[n] * invfac[m] % p * invfac[n - m] % p; }实操心得在实现lucas函数时一定要先判断if (m 0)返回 1这是递归的基准情况。另外在C函数中判断if (n m)返回 0 非常重要因为在递归过程中n%p有可能小于m%p此时的组合数定义为 0。3.3 中国剩余定理CRT的合并实现我们有方程组x ≡ ai (mod mi)i1 to 4mi两两互质。CRT的标准求解公式为计算总模数M m1 * m2 * m3 * m4。对于每个i计算Mi M / mi。计算ti是Mi在模mi意义下的逆元即Mi * ti ≡ 1 (mod mi)。解为x Σ(ai * Mi * ti) mod M。实现代码long long crt(const vectorlong long a, const vectorlong long m) { long long M 1, x 0; int k a.size(); for (int i 0; i k; i) M * m[i]; for (int i 0; i k; i) { long long Mi M / m[i]; // 求 Mi 模 m[i] 的逆元 ti long long ti inv(Mi, m[i]); // 需要实现扩展欧几里得求逆元 x (x a[i] * Mi % M * ti % M) % M; } return (x % M M) % M; // 确保返回正数 }其中inv(a, mod)是求a在模mod下的逆元。由于这里的mod即mi都是质数可以用快速幂fast_pow(a, mod-2, mod)来求。但为了通用性通常使用扩展欧几里得算法。注意事项在累加x的过程中每次乘法后都要对M取模防止中间结果溢出。即使使用long longai * Mi * ti这三个数相乘也可能溢出所以需要步步取模。3.4 快速幂算法快速幂是基础但必须写对的算法。用于最后的G^sum_mod mod MOD计算以及在求逆元时计算a^(p-2) mod p。long long fast_pow(long long base, long long exp, long long mod) { long long result 1; base % mod; // 先取模防止 base 过大 while (exp 0) { if (exp 1) { result (result * base) % mod; } base (base * base) % mod; exp 1; } return result; }4. 完整代码实现与逐段解析下面我将结合代码详细讲解每个模块的实现和联动。我们假设输入为N和G模数MOD 999911659。#include iostream #include vector #include cmath #include algorithm using namespace std; typedef long long ll; const ll MOD 999911659; // MOD-1 的质因子分解 ll primes[4] {2, 3, 4679, 35617}; ll fac[36000]; // 最大质因子是35617数组开大一点 ll invfac[36000]; ll a[4]; // 存储 sum 对每个质因子取模的结果 vectorll divisors; // 快速幂 ll qpow(ll base, ll exp, ll mod) { ll res 1; base % mod; while (exp) { if (exp 1) res (res * base) % mod; base (base * base) % mod; exp 1; } return res; } // 扩展欧几里得求逆元 (ax ≡ 1 mod m) 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, y, x); y - (a / b) * x; return d; } ll inv(ll a, ll mod) { ll x, y; exgcd(a, mod, x, y); return (x % mod mod) % mod; } // 预处理阶乘和阶乘逆元 (模 p) void init_fac(ll p) { fac[0] 1; for (ll i 1; i p; i) { fac[i] fac[i-1] * i % p; } // 费马小定理求阶乘逆元 invfac[p-1] qpow(fac[p-1], p-2, p); for (ll i p-2; i 0; --i) { invfac[i] invfac[i1] * (i1) % p; } } // 计算小组合数 C(n, m) % p (要求 n, m p) ll C_small(ll n, ll m, ll p) { if (n m) return 0; return fac[n] * invfac[m] % p * invfac[n - m] % p; } // Lucas定理计算 C(n, m) % p ll lucas(ll n, ll m, ll p) { if (m 0) return 1; return (C_small(n % p, m % p, p) * lucas(n / p, m / p, p)) % p; } // 中国剩余定理合并 ll crt() { ll M MOD - 1; // 即 999911658 ll res 0; for (int i 0; i 4; i) { ll Mi M / primes[i]; ll ti inv(Mi, primes[i]); // 求 Mi 模 primes[i] 的逆元 res (res a[i] * Mi % M * ti % M) % M; } return (res % M M) % M; } int main() { ll N, G; cin N G; // 特判如果 G 是 MOD 的倍数 if (G % MOD 0) { cout 0 endl; return 0; } // 1. 枚举 N 的所有因数 for (ll i 1; i * i N; i) { if (N % i 0) { divisors.push_back(i); if (i ! N / i) { divisors.push_back(N / i); } } } // 2. 对每个质因子 p计算 sum_p Σ lucas(N, d, p) mod p for (int i 0; i 4; i) { ll p primes[i]; init_fac(p); // 预处理模 p 下的阶乘 ll sum_p 0; for (ll d : divisors) { sum_p (sum_p lucas(N, d, p)) % p; } a[i] sum_p; // 记录余数 } // 3. 用中国剩余定理合并得到 sum_mod sum % (MOD-1) ll sum_mod crt(); // 4. 计算最终答案 G^sum_mod % MOD ll ans qpow(G, sum_mod, MOD); cout ans endl; return 0; }代码逐段解析全局定义与输入定义了模数、质因子数组、阶乘数组、余数数组和存储因数的向量。读入N和G。特判首先检查G % MOD 0这是欧拉定理应用的前提。如果为真直接输出0结束。枚举因数使用O(sqrt(N))的方法找出N的所有正因数存入divisors向量。核心循环对每个质因子for (int i 0; i 4; i)遍历四个质因子2, 3, 4679, 35617。init_fac(p)针对当前质数p预处理0!到(p-1)!的阶乘及其逆元。这是Lucas定理能高效计算的基础。内层循环for (ll d : divisors)遍历N的每个因数d。sum_p (sum_p lucas(N, d, p)) % p使用Lucas定理计算C(N, d) mod p并累加到sum_p中同时保持取模。将最终累加结果sum_p存入a[i]。这样我们就得到了四个同余方程的余数a[0]...a[3]。中国剩余定理合并调用crt()函数根据四个余数a[i]和模数primes[i]计算出sum_mod sum % (MOD-1)。最终计算与输出使用快速幂qpow(G, sum_mod, MOD)计算最终答案并输出。5. 常见问题与调试心得这道题在实现和提交时很容易遇到各种问题。下面是我总结的几个常见坑点和调试技巧。5.1 溢出问题这是数论题最经典的错误来源。中间结果溢出即使在long long64位范围内三个long long相乘也可能溢出。例如在crt()函数中a[i] * Mi * ti就可能超出2^63-1。务必在每次乘法后立即取模(a[i] * Mi % M * ti) % M。快速幂中的溢出在qpow函数中base * base也可能溢出所以在函数开头先base % mod是很好的习惯。阶乘预处理溢出预处理fac[i] fac[i-1] * i % p时i最大为p-1而p最大为35617fac[i-1] * i不会超过1e10在long long范围内是安全的。排查技巧当你怀疑溢出时可以在关键计算步骤后添加调试输出打印中间变量的值看看是否突然变成了负数这是补码溢出的典型表现。5.2 边界条件与特判G % MOD 0这是最重要的特判。如果不加当G是MOD倍数时gcd(G, MOD) ! 1欧拉定理不成立降幂步骤就是错误的会导致答案错误。N 的因数 1 和 N枚举因数时1和N本身也是因数必须包含在内。组合数C(N, 1) N,C(N, N) 1它们对sum有贡献。Lucas 递归基准情况lucas函数中必须判断if (m 0) return 1。因为当m为0时组合数C(n, 0) 1。C_small 中的 n m 判断在递归调用中n % p可能小于m % p此时C(n%p, m%p)应为0。这个判断必须加上。5.3 中国剩余定理CRT的实现细节逆元的正确性crt()中需要求Mi模primes[i]的逆元ti。确保你的inv函数能正确工作。由于primes[i]是质数用qpow(Mi, primes[i]-2, primes[i])求逆元更简洁且不易错。最终解的模数CRT求出的解x是模M意义下的。M MOD-1。最后返回(x % M M) % M是为了保证结果是[0, M-1]范围内的最小非负整数。模数一致性在计算Mi M / primes[i]时M必须是所有模数的乘积即999911658。5.4 性能优化点因数枚举O(sqrt(N))枚举已经足够高效。对于N1e9循环次数约3e4到4e4次完全可以接受。预处理复用对于每个质因子p我们都需要预处理阶乘。由于p很小最大35617预处理是O(p)的总共做4次开销很小。Lucas递归深度由于p最小是2递归深度大约是log_p(N)对于N1e9深度最多30层递归开销可以忽略。5.5 调试与测试建议当你写完后可以用一些小的样例进行测试。样例1N2, G3因数1, 2sum C(2,1)C(2,2)213分别计算 mod 2,3,4679,35617 的余数都是 3 mod p。CRT合并后 sum_mod 3。答案 3^3 mod 999911659 27。 你可以手动计算验证。样例2N4, G2因数1, 2, 4sum C(4,1)C(4,2)C(4,4)46111计算 mod 2: 11%21; mod 3: 11%32; mod 4679: 11; mod 35617: 11。解同余方程组得到 sum_mod。最终计算 2^sum_mod mod 999911659。如果小样例对了但提交到OJOnline Judge还是Wrong Answer可以尝试检查是否遗漏了G % MOD 0的特判。检查crt()函数中乘法取模是否写全了% M。检查lucas函数中的C_small调用是否包含了n m返回 0 的判断。用cout打印出中间变量比如四个余数a[i]、CRT合并后的sum_mod与手算或对拍程序的结果进行对比。这道题综合性很强几乎考察了数论竞赛中的所有常用技巧。把它吃透对于理解模运算、组合数计算、定理的综合应用有极大的好处。在实际编码中耐心和细心是关键尤其是处理取模和边界条件时。希望这篇详细的拆解能帮助你彻底掌握“古代猪文”这道经典题目。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价