当前位置: 首页 > news >正文

告别费马小定理!用线性递推法在C++里高效搞定逆元(附完整代码)

告别费马小定理!用线性递推法在C++里高效搞定逆元(附完整代码)

在算法竞赛和高性能计算领域,模运算中的逆元计算一直是困扰开发者的痛点。无论是计算组合数还是解决数论问题,传统方法往往面临效率瓶颈。想象一下,当你在ACM赛场遇到需要快速计算十万级别逆元的问题时,费马小定理的O(n log p)复杂度会让你与奖牌失之交臂——这正是线性递推法大显身手的时刻。

1. 为什么我们需要更好的逆元算法

逆元在模运算中的地位,就像倒数在普通乘法运算中一样关键。它让我们能在模数下实现"除法"操作,这在组合数学、密码学等领域不可或缺。但传统方法各有局限:

  • 扩展欧几里得法:每次计算独立,无法利用之前结果
  • 费马小定理:要求模数为质数且复杂度较高
  • 预处理法:需要额外空间存储阶乘等中间结果

特别是在计算组合数C(n,k) mod p时,我们需要频繁调用逆元运算。当n达到1e6量级时,传统方法的性能瓶颈就暴露无遗。线性递推法正是在这种需求下应运而生,它能以O(n)的时间复杂度预处理所有逆元。

实际测试表明,当n=1e6时,线性递推法比费马小定理快约50倍

2. 线性递推法的数学原理

线性递推法的核心在于发现逆元之间的递推关系。让我们从模运算的基本性质出发:

设p为质数,i<p,我们想求i的逆元inv[i]。定义:

  • 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]:

t × inv[k] ≡ -inv[i] (mod p)

最终得到递推公式:

inv[i] = (p - t × inv[k] % p) % p

或者用代码更直观地表示:

inv[i] = (p - p/i * inv[p%i] % p) % p;

这个公式的美妙之处在于,它让我们能用更小的数的逆元来计算当前数的逆元,形成链式反应。

3. 完整实现与边界处理

理解了数学原理后,我们来看具体实现。以下是完整的C++实现代码:

#include <iostream> using namespace std; typedef long long ll; const int MAXN = 1e6 + 10; // 根据题目需求调整 ll inv[MAXN]; void precompute_inv(int n, int p) { inv[1] = 1; // 初始条件 for(int i = 2; i <= n; ++i) { inv[i] = (p - p/i) * inv[p%i] % p; } } int main() { int n = 1e6, p = 1e9+7; // 示例参数 precompute_inv(n, p); // 验证输出前10个逆元 for(int i = 1; i <= 10; ++i) { cout << "inv[" << i << "] = " << inv[i] << endl; } return 0; }

关键实现细节:

  1. 初始化:必须设置inv[1] = 1,这是递推的起点
  2. 类型选择:使用long long防止中间结果溢出
  3. 负数处理:公式中的(p - ...)确保结果为正
  4. 模数限制:要求p必须是质数且大于n

常见错误处理:

  • 当p不是质数时,某些数可能没有逆元
  • 当i ≥ p时,结果无定义
  • 数组大小不足导致越界

4. 阶乘逆元的线性计算

在组合数计算中,我们经常需要阶乘的逆元。可以结合线性递推法进一步优化:

const int MOD = 1e9+7; ll fact[MAXN], inv_fact[MAXN]; void precompute_factorial_inv(int n) { fact[0] = inv_fact[0] = 1; for(int i = 1; i <= n; ++i) { fact[i] = fact[i-1] * i % MOD; } // 先用线性递推法计算单个逆元 inv_fact[n] = quick_pow(fact[n], MOD-2); // 费马小定理计算n!的逆元 // 反向递推计算阶乘逆元 for(int i = n-1; i >= 1; --i) { inv_fact[i] = inv_fact[i+1] * (i+1) % MOD; } }

这种组合方法的时间复杂度仍然是O(n),但能同时提供阶乘和阶乘逆元,极大简化组合数计算:

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

5. 实战应用与性能对比

让我们看一个实际比赛题目中的应用示例。假设题目要求计算:

∑C(n,i)² mod 1e9+7,其中n≤1e6

使用传统方法:

ll ans = 0; for(int i = 0; i <= n; ++i) { ll c = C(n, i); // 每次用费马小定理计算 ans = (ans + c * c) % MOD; }

时间复杂度:O(n log MOD)

使用预处理法:

precompute_factorial_inv(n); ll ans = 0; for(int i = 0; i <= n; ++i) { ll c = fact[n] * inv_fact[i] % MOD * inv_fact[n-i] % MOD; ans = (ans + c * c) % MOD; }

时间复杂度:O(n)

性能对比表:

方法n=1e5时间n=1e6时间内存使用
费马小定理350ms3500msO(1)
线性递推15ms150msO(n)

虽然线性递推法需要额外O(n)空间,但在时间敏感的场景下,这种trade-off是完全值得的。

6. 高级技巧与注意事项

  1. 动态模数处理:当模数不固定时,可以这样实现:
vector<int> precompute_inv(int n, int p) { vector<int> inv(n+1); inv[1] = 1; for(int i = 2; i <= n; ++i) { inv[i] = (p - p/i) * inv[p%i] % p; } return inv; }
  1. 内存优化:如果只需要部分逆元,可以用滚动变量代替数组:
int compute_single_inv(int i, int p) { if(i == 1) return 1; return (p - p/i) * compute_single_inv(p%i, p) % p; }
  1. 常见陷阱
  • 模数必须是质数
  • 初始条件inv[1]=1不能遗漏
  • 中间结果可能溢出,确保使用足够大的数据类型
  1. 调试技巧
// 验证逆元是否正确 assert(1LL * i * inv[i] % p == 1);

在实际比赛中,我通常会这样组织代码:

#include <bits/stdc++.h> using namespace std; const int N = 1e6+5, MOD = 1e9+7; int inv[N], fact[N], inv_fact[N]; void precompute() { inv[1] = fact[0] = inv_fact[0] = 1; for(int i = 2; i < N; ++i) inv[i] = MOD - MOD/i * inv[MOD%i] % MOD; for(int i = 1; i < N; ++i) fact[i] = 1LL * fact[i-1] * i % MOD; inv_fact[N-1] = 1; for(int i = N-2; i >= 1; --i) inv_fact[i] = 1LL * inv_fact[i+1] * (i+1) % MOD; } int C(int n, int k) { if(k < 0 || k > n) return 0; return 1LL * fact[n] * inv_fact[k] % MOD * inv_fact[n-k] % MOD; } int main() { precompute(); // 解决问题... }

这种预处理方式虽然增加了约10行代码,但能让后续的所有组合数计算都变成O(1)操作。在最近的区域赛中就有一道题,直接使用这个模板比现场推导的选手快了近30分钟。

http://www.jsqmd.com/news/672803/

相关文章:

  • python+requests实现的接口自动化测试
  • 前端八股文面经大全:来未来前端实习一面(2026-04-17)·面经深度解析
  • 拯救者R7000用户看过来:保姆级教程,让你的非华为笔记本也能和MatePad Pro多屏协同
  • 电源硬件设计----LDO选型与热设计实战指南
  • TVBoxOSC:5分钟快速上手电视盒子智能控制终极指南
  • GD32F407 USB CDC虚拟串口调试实战:从枚举失败到稳定收发数据的避坑指南
  • Maxwell Simplorer Simulink 永磁同步电机矢量控制联合仿真
  • 从职场回归考场:一位十年工龄工程师的MEM备考实战复盘
  • 告别objdump!用Python的pwntools一键生成汇编对应的hex机器码(附Mac/Linux安装避坑)
  • 154基于单片机无线多机WIFI通讯通信系统设计
  • MATLAB chirp函数:从基础语法到雷达信号仿真实战
  • 从本地Jupyter到云端Colab:无缝迁移你的PyTorch/TensorFlow项目实战
  • 如何实现AudioRecord内录r_submix模式系统Speaker正常发声?-学员作业
  • 国内业界首个AI一键生成手绘思维导图的脑图产品来!万兴科技旗下万兴脑图重磅焕新
  • 手机号码归属地查询系统的架构设计与实现
  • 图像图片照片风格转换API接口介绍
  • 别再一上来就调包了!统计建模新手最容易踩的5个坑(附Python/R实战避坑清单)
  • 用TCRT5000传感器改造玩具车:低成本搭建竞赛级Arduino循迹机器人
  • 鸿蒙开发入门指南:鸿蒙canvas实操——快速掌握自定义图表组件
  • Sqoop和DataX到底怎么选?从我们的数仓迁移实战聊聊工具选型
  • 保姆级教程:用YOLOv11+PyQt5做个垃圾分类小助手(附完整代码和数据集)
  • Obsidian Weread插件:一键同步微信读书笔记到知识库的高效解决方案
  • MAA明日方舟自动化助手:从零开始的全功能使用指南
  • 田纳西男子多次黑入美国最高法院文件系统:安全防护与访问控制剖析
  • 别再折腾WSL2了!Windows 10/11一键搞定Docker Desktop安装(附保姆级排错指南)
  • 别再调参了!用KELM(核极限学习机)做回归预测,Matlab代码实战与性能对比
  • 免费解锁iPhone激活锁:使用applera1n工具完整指南
  • 终极免费卡拉OK游戏:UltraStar Deluxe完整入门指南
  • Golang怎么设置响应状态码_Golang如何用WriteHeader返回404或500状态【基础】
  • 如何用BabelDOC轻松解决PDF翻译难题:5步完整指南