news 2026/4/19 4:03:54

告别费马小定理!用线性递推O(n)批量求逆元,组合数计算效率翻倍(附C++代码)

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
告别费马小定理!用线性递推O(n)批量求逆元,组合数计算效率翻倍(附C++代码)

告别费马小定理!用线性递推O(n)批量求逆元,组合数计算效率翻倍(附C++代码)

在算法竞赛和编程面试中,组合数计算是一个高频出现的难题。想象一下这样的场景:你正在参加ACM比赛,面对一道需要计算大量组合数的问题,使用传统的费马小定理逐个求逆元,结果程序因为超时被无情地判为TLE。这种挫败感,相信很多选手都深有体会。

组合数计算的效率瓶颈往往在于逆元的求解。传统方法如费马小定理或扩展欧几里得算法,虽然能正确求出单个逆元,但当需要处理1到n所有数的逆元时,时间复杂度会飙升至O(n log p),这在n较大时(比如1e6以上)会成为性能杀手。今天,我将分享一种能在O(n)时间内批量求出所有逆元的线性递推方法,让你的组合数计算速度提升一个数量级。

1. 逆元:组合数计算的关键瓶颈

在模运算的世界里,除法并不像加减乘那样直接。为了计算a/b mod p,我们需要找到b的乘法逆元,即一个数x使得b*x ≡ 1 mod p。这个x就是b在模p下的逆元,记作inv(b)。

组合数公式C(n, k) = n! / (k! * (n-k)!)在模p下的计算,需要分别求出k!和(n-k)!的逆元。如果对每个组合数都独立计算这些逆元,效率会非常低下。这就是为什么我们需要一种能批量预处理所有逆元的方法。

传统求逆元的方法主要有两种:

  • 费马小定理:当p是质数时,inv(a) = a^(p-2) mod p。使用快速幂,单次求逆元时间为O(log p)。
  • 扩展欧几里得算法:解方程ax + py = 1,得到的x就是a的逆元。时间复杂度也是O(log p)。

当n=1e6时,传统方法的总时间复杂度是O(n log p),而线性递推方法可以将这个复杂度降至O(n),效率提升非常显著。

2. 线性递推求逆元的数学原理

线性递推求逆元的核心在于发现逆元之间的递推关系。假设我们要求1到n所有数在模p下的逆元,可以按照以下步骤进行:

  1. 初始化inv[1] = 1(因为1的逆元显然是它自己)
  2. 对于i > 1,利用递推公式:
    inv[i] = (p - p/i) * inv[p % i] % p

这个公式的推导过程如下:

设t = p/i,k = p%i,则有p = i*t + k。在模p下,这等价于:

i*t + k ≡ 0 mod p => i*t ≡ -k mod p

两边乘以inv(i)*inv(k)(即i和k的逆元):

t*inv(k) ≡ -inv(i) mod p => inv(i) ≡ -t*inv(k) mod p

由于k = p%i < i,inv(k)已经在前面的迭代中计算过,因此可以递推求出inv(i)。

3. 完整实现:从逆元到阶乘逆元

掌握了线性递推求逆元的方法后,我们可以进一步优化组合数的计算。完整的预处理流程包括三个步骤:

  1. 预处理阶乘数组fact[i] = i! mod p
  2. 预处理逆元数组inv[i] = i^(-1) mod p
  3. 预处理阶乘逆元数组fact_inv[i] = (i!)^(-1) mod p

以下是完整的C++实现代码:

#include <iostream> using namespace std; typedef long long ll; const int MAXN = 1e6 + 10; // 根据题目需求调整大小 const int MOD = 1e9 + 7; // 常见的质数模数 ll fact[MAXN], inv[MAXN], fact_inv[MAXN]; void precompute(int n) { // 初始化 fact[0] = 1; for (int i = 1; i <= n; i++) { fact[i] = fact[i-1] * i % MOD; } // 线性递推求逆元 inv[1] = 1; for (int i = 2; i <= n; i++) { inv[i] = (MOD - MOD/i) * inv[MOD%i] % MOD; } // 计算阶乘逆元 fact_inv[0] = 1; for (int i = 1; i <= n; i++) { fact_inv[i] = fact_inv[i-1] * inv[i] % MOD; } } ll comb(int n, int k) { if (k < 0 || k > n) return 0; return fact[n] * fact_inv[k] % MOD * fact_inv[n-k] % MOD; } int main() { int n = 1e6; // 预处理的范围 precompute(n); // 示例:计算C(1000000, 500000) cout << comb(1000000, 500000) << endl; return 0; }

这段代码首先预处理了阶乘、逆元和阶乘逆元三个数组,之后任何组合数查询都可以在O(1)时间内完成。预处理的时间复杂度是O(n),空间复杂度也是O(n)。

4. 性能对比与实战应用

为了直观展示线性递推方法的优势,我做了以下性能对比测试(环境:Intel i7-9700K,n=1e6,p=1e9+7):

方法预处理时间(ms)单次查询时间(ns)
费马小定理1200300
扩展欧几里得1000400
线性递推5010

从表中可以看出,线性递推方法在预处理阶段比传统方法快20倍以上,查询速度也快30倍左右。这种优势在算法竞赛中往往是决定性的。

在实际应用中,这种方法特别适合以下场景:

  • 需要多次查询组合数的问题(如动态规划中涉及组合数的状态转移)
  • n很大但查询次数更多的情况(如n=1e6,查询次数1e7)
  • 对时间要求极其严格的在线评测题目

提示:虽然线性递推方法效率很高,但需要注意p必须是质数,且n不能超过预处理的MAXN大小。在编程竞赛中,通常题目会给出质数模数,如1e9+7或998244353。

5. 常见问题与优化技巧

在实际使用中,可能会遇到一些问题和陷阱。以下是我总结的几个常见问题及解决方案:

  1. 模数不是质数怎么办?

    • 线性递推方法要求模数p是质数。如果p不是质数,可以考虑质因数分解后使用中国剩余定理合并结果,或者改用扩展卢卡斯定理。
  2. 内存限制严格时如何优化?

    • 如果只需要组合数而不需要单独的逆元,可以省略inv数组,直接计算阶乘逆元:
      fact_inv[n] = pow(fact[n], MOD-2, MOD); // 费马小定理求一次逆元 for (int i = n-1; i >= 0; i--) { fact_inv[i] = fact_inv[i+1] * (i+1) % MOD; }
  3. 如何处理非常大的n(如n=1e7)?

    • 可以考虑分段预处理,或者使用内存更高效的数据结构。在C++中,使用vector代替静态数组可以更灵活地管理内存。
  4. 多组测试数据时的优化:

    • 如果多组测试数据的n不同但p相同,可以预处理到最大可能的n,避免重复计算。
// 全局预处理到最大值 const int MAX_PRE = 1e7; void global_precompute() { static bool precomputed = false; if (!precomputed) { // ...预处理代码... precomputed = true; } }

6. 在线评测系统中的实战技巧

在ACM/ICPC等编程竞赛中,时间就是生命。以下是我总结的几个实战技巧:

  1. 模板准备:将预处理代码封装成模板,比赛时直接复制粘贴,节省编码时间。

  2. 快速调试:预处理范围MAXN设置错误是常见bug。可以在本地测试时加入断言检查:

    assert(n < MAXN); // 确保查询不会越界
  3. 边界条件处理:组合数C(n,k)在k<0或k>n时应返回0,这个判断不能遗漏。

  4. 性能测试:在本地用最大数据量测试预处理时间,确保不会在比赛中超时。

  5. 空间优化:如果题目只需要特定几个组合数,可以考虑按需计算而非全预处理。

// 按需计算的组合数函数(适用于查询次数少的情况) ll comb_ondemand(int n, int k) { if (k < 0 || k > n) return 0; ll res = 1; for (int i = 1; i <= k; i++) { res = res * (n - k + i) % MOD; res = res * inv[i] % MOD; // 需要预先处理好inv数组 } return res; }

在洛谷P3811【模板】乘法逆元这道经典题目中,线性递推方法可以轻松通过n=3e6的数据规模,而传统方法则很难在规定时间内完成。这也是为什么越来越多的选手将线性递推求逆元作为必备技能。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/19 4:03:48

深度解析:ABAP2XLSX技术架构与Excel报表生成优化

深度解析&#xff1a;ABAP2XLSX技术架构与Excel报表生成优化 【免费下载链接】abap2xlsx Generate your professional Excel spreadsheet from ABAP 项目地址: https://gitcode.com/gh_mirrors/ab/abap2xlsx ABAP2XLSX是一个专业的开源ABAP库&#xff0c;用于在SAP系统中…

作者头像 李华
网站建设 2026/4/19 3:59:25

保姆级教程:用Thonny IDE给ESP32-CAM烧录MicroPython固件(含CH340驱动安装)

从零玩转ESP32-CAM&#xff1a;Thonny环境搭建与MicroPython固件烧录全指南 第一次拿到ESP32-CAM开发板时&#xff0c;很多开发者都会被它小巧的体积和强大的功能所吸引——这款集成了摄像头的开发板能够轻松实现图像采集、人脸识别等酷炫功能。但当你兴冲冲地准备大展身手时&a…

作者头像 李华
网站建设 2026/4/19 3:58:14

Cadence Allegro 16.6实战:从设计到生产的PCB光绘文件精准输出指南

1. 前期检查&#xff1a;确保设计万无一失 在Allegro 16.6中输出光绘文件前&#xff0c;必须像建筑师验收大楼一样严格检查PCB设计。我见过太多因为漏检导致生产事故的案例&#xff0c;有一次就因为一个未连接的过孔导致整批板子报废。下面这些检查项都是我踩坑后总结的必做清单…

作者头像 李华
网站建设 2026/4/19 3:55:20

从零构建:基于PyTorch与小型中文语料库的GPT对话模型实战

1. 为什么选择PyTorch搭建小型中文GPT 作为一个在个人电脑上就能跑起来的实验项目&#xff0c;PyTorch绝对是我们的首选框架。我当年第一次尝试用TensorFlow实现语言模型时&#xff0c;光是静态计算图就把我折腾得够呛。PyTorch的动态图机制对初学者友好得多&#xff0c;就像用…

作者头像 李华
网站建设 2026/4/19 3:47:14

全球知名3D打印企业2025年营收情况汇总

根据《Wohlers Report 2026》&#xff0c;2025年全球3D打印市场规模为242亿美元&#xff08;约1652亿元&#xff09;&#xff0c;相比上一年的219亿美元增长10.9%。另外根据中国增材制造产业联盟的统计&#xff0c;2025年国内3D打印行业市场规模已达到700亿元。不过&#xff0c…

作者头像 李华