CSP-S 提高级课程中心 / 数学进阶 / 第43讲
CSP-S 提高级 · 数学进阶 · s6-02

同余式 与 逆元

取模世界里的"加减乘"都太平凡,唯一麻烦的是除法 —— 掌握逆元,你就能在模意义下随心所欲地做除法,把"分数取模"变成一次乘法。

6 大模块 O(log b) 快速幂 3 种逆元求法 3 个模板题案例 12 道洛谷原题
本讲 · 第 43/52 讲同余式与逆元
模块 01

情景导航

从一个真实的赛场翻车现场出发,理解为什么"取模"必须重新发明一套除法。

故事赛场上消失的小数点

CSP-S 第二轮,小 Q 遇到了这样一道题:"给定整数 a 与 b(b 与 19260817 互质),求 a/b 在模 19260817 意义下的值。"

小 Q 心想:这有什么难的?直接算 double ans = (double)a / b; 然后输出不就行了?他信心满满地交了。

结果:0 分。答案输出是 0.1431…,而评测机期待的是一个 0 到 19260816 之间的大整数。

小 Q 又改成 (a % p) / (b % p),更是错得离谱 —— 取模之后的除法在数学上根本不等价于原来的除法。

教练在黑板上写下一行字:"模意义下没有除法,只有乘法。想除,先求逆元。"

a / b (mod p)  =  a × inv(b) (mod p)

小 Q 这才明白:在模运算的世界里,"除以 b" 这件事必须被翻译成"乘以 b 的逆元"。而求逆元之前,还要先学会另一个基础工具 —— 快速幂,它能把 10¹⁸ 次方的计算压进 60 次乘法。

💡 故事背后的数学结构

整数集在模 m 下被分成 m 个"余数类",加减乘三种运算都可以在余数类上直接进行 —— 这就是同余式的意义。唯独除法不行,因为"能否除"取决于除数是否与模数互质。

当 gcd(a, m) = 1 时,方程 a·x ≡ 1 (mod m) 恰好有唯一解,这个解就是 a 的乘法逆元。有了它,整数的除法就在模世界里"复活"了。

提高组的一切计数类题目(组合数、概率期望、多项式)几乎都建立在这个小技巧上,所以它是数论模块最必要的一块基石。

①生活类比
🕐

钟表 = 模 12

下午 3 点也可以叫 15 点。15 和 3 在"模 12"意义下是同一个数,指向同一个位置。

→ 15 ≡ 3 (mod 12)
📅

星期 = 模 7

今天是周三,10 天后是周几?只看 10 mod 7 = 3,周三往后数 3 天。

→ 加法保持同余
🏃

跑道套圈 = 余数类

400 米跑道上,跑 1 圈和跑 5 圈停在同一位置。所有差为 400 的倍数的距离都"等价"。

→ 等价类 / 余数类
🔐

保险箱转盘 = 逆元

密码盘向右转 3 格锁上,要打开就得向左转 3 格 —— "抵消"掉某次旋转的那次旋转。

→ a × inv(a) ≡ 1
②核心定义

同余式:设 m 为正整数。若 m 整除 a - b,就说 a 与 b 关于模 m 同余,记作 a ≡ b (mod m)。例如 15 ≡ 3 (mod 12)、-1 ≡ 6 (mod 7)。

余数类(同余类):把所有模 m 同余的整数放在一起构成一个集合。模 m 下恰好有 m 个余数类,通常用 0, 1, …, m-1 作代表元。

快速幂:把指数 b 写成二进制并按位平方累乘,在 O(log b) 次乘法内算出 aᵇ,还能顺带完成取模。

乘法逆元:若 a·x ≡ 1 (mod m),则称 x 为 a 在模 m 下的逆元,记作 a⁻¹ 或 inv(a)。逆元存在当且仅当 gcd(a, m) = 1。

分数取模:(a / b) mod p = a · inv(b) mod p(要求 gcd(b, p) = 1)。这是把有理数运算搬进模世界的唯一通道。

📌 本讲核心知识地图

板块核心内容对应大纲
同余式a ≡ b (mod m) ⟺ m | (a-b);加减乘保持、除法不保持【5】同余式
快速幂二进制分解指数,O(log b) 完成 aᵇ mod p同余式的核心工具
逆元的定义a·x ≡ 1 (mod m),存在 ⟺ gcd(a,m)=1【7】模运算意义下的逆元
逆元三求法费马小定理(p 素数)/ 扩展欧几里得 / 线性递推打表【7】模运算意义下的逆元
分数取模(a/b) mod p = a·inv(b) mod p【7】模运算意义下的逆元
③核心口诀
🔑 同余

加减乘号随便进出,除号进门先办证;
负数取模加个 m,结果落在 0 到 m-1。

🔑 快速幂

底数一路平方走,指数只看奇与偶;
为奇乘进答案里,为偶跳过继续走,六十次乘法算十亿次方。

🔑 逆元

逆元存在看互质,三种求法分场景:
模是素数用费马,通用问题扩欧几,批量打表线性推。

模块 02

知识探究

先建立同余式的运算规则,再拿下快速幂这个工具,最后用三种方法求逆元,把分数取模彻底打通。

1同余式与余数类 基础

定义:对正整数 m,若 m | (a - b),记作 a ≡ b (mod m),读作"a 同余于 b 模 m"。等价说法有三条,考试里经常互相替换使用:

1
➗
整除说法

m | (a − b)

2
🪞
余数说法

a mod m == b mod m

3
🧩
差量说法

存在整数 k 使 a = b + k·m

📐 同余的四则运算规则

设 a ≡ b (mod m),c ≡ d (mod m),则:

① 加法保持:a + c ≡ b + d (mod m)

② 减法保持:a - c ≡ b - d (mod m)

③ 乘法保持:a × c ≡ b × d (mod m),特别地 a^k ≡ b^k (mod m)

④ 除法不保持:一般地 ac ≡ bc (mod m) 时不能随便约去 c —— 只有 gcd(c, m) = 1(即 c 在模 m 下有逆元)时才可以约。反例:6 ≡ 0 (mod 3),两边除以 3 得 2 与 0,但 2 ≢ 0 (mod 3),因为 3 与模数 3 不互质;而 4 ≡ 1 (mod 3) 两边除以 2 得 2 与 2 恰好同余,那是因为 gcd(2, 3) = 1,2 在模 3 下有逆元。

这就是口诀里说的:加减乘随便进出,除号进门先办证(先求逆元)。

✅ 保持同余的运算

(a + b) mod m = ((a mod m) + (b mod m)) mod m

(a - b) mod m = ((a mod m) - (b mod m) + m) mod m

(a × b) mod m = ((a mod m) × (b mod m)) mod m

这三条让"边算边取模"成为可能,是所有取模题的合法性来源。

❌ 不保持同余的运算

(a / b) mod m ≠ ((a mod m) / (b mod m)) mod m

反例:m = 7,a = 10,b = 5。左边 (10/5) mod 7 = 2;右边 (3 / 5) mod 7 无整数意义。

另外 比较大小、开方、取对数 也都不能直接搬到模世界里。

0 / 12 3 6 模 12 的钟面:所有整点落进 12 个格子
🕐 余数类:把无限的整数装进 m 个格子
  1. 模 12 下,-3、9、21、33 全部落进"9 点"这个格子。
  2. 格子之间可以做加减乘:3 点 + 10 小时 = 1 点,对应 (3+10) mod 12 = 1。
  3. 但"6 点除以 2"没有意义 —— 是 3 点还是 9 点?除法在钟面上天然不唯一。
  4. 如果限定在"与 12 互质的格子上"(1、5、7、11),除法就重新变得唯一 —— 这就是逆元存在条件的几何直觉。
负数取模的坑:C++ 中 -1 % 7 的结果是 -1,而不是 6。要得到"数学上正确的余数",必须写成 (x % m + m) % m。这个写法在数论题里出现频率极高。
2快速幂:O(log b) 算 aᵇ mod p 必背模板

暴力算 aᵇ 需要 b 次乘法。当 b 达到 10¹⁸ 时,这是不可能完成的任务。快速幂的核心洞察是:平方可以把指数翻倍,于是把指数按二进制拆开,就能把乘法次数压到 log₂b 级别。

📐 二进制分解的推导

把指数 b 写成二进制 b = 2^{k₁} + 2^{k₂} + …,于是

a^b = a^(2^{k₁}) × a^(2^{k₂}) × …

而 a^(2^{i+1}) = (a^(2^i))²,即只要不断把底数平方,就能依次得到 a¹、a²、a⁴、a⁸、… 这些"权重"。指数 b 的二进制中哪一位是 1,就把对应的权重乘进答案。

整个过程最多执行 ⌊log₂b⌋ + 1 轮,每轮做 1~2 次乘法,因此时间复杂度 O(log b)。

📊 指数分解示意图:b = 13 = (1101)₂
bit31
bit21
bit10
bit01
13 = 8 + 4 + 1 → a¹³ = a⁸ × a⁴ × a¹(bit1 为 0,跳过 a²)
🎬 交互演示:手算 3¹³ mod 100 的平方累乘过程
1初始状态:res = 1,base = 3,exp = 13 = (1101)₂。我们从最低位 bit0 开始逐位处理。
2bit0 = 1:把 base 乘进答案 → res = 1 × 3 = 3;然后把 base 平方 → base = 3² = 9;exp 右移 → 110。
3bit1 = 0:这一位是 0,不乘。只做平方 → base = 9² = 81;exp 右移 → 11,此时 res 仍是 3。
4bit2 = 1:把 base 乘进答案 → res = 3 × 81 = 243 ≡ 43 (mod 100);再平方 → base = 81² = 6561 ≡ 61;exp 右移 → 1。
5bit3 = 1:把 base 乘进答案 → res = 43 × 61 = 2623 ≡ 23 (mod 100);再平方 → 61² = 3721 ≡ 21(后面用不到了);exp 右移 → 0。
6指数归零,循环结束,返回 res = 23。验算:3¹³ = 1594323,末两位正是 23 ✓。全程只做了 5 轮、共 6 次乘法。
💡 记忆要点:循环的每一轮都"先看末位、再平方、最后右移",三步顺序不能乱;最后的平方虽然用不到,但代码里统一执行,省去额外判断。
STEP 1底数取模
STEP 2res = 1 % p
STEP 3看末位
STEP 4乘进答案
STEP 5底数平方
STEP 6指数右移
悬停/自动轮播可查看每一步的作用 —— 六步循环,就是快速幂的全部
📈 递归写法
  • 思路:aᵇ = (a^(b/2))² ,b 为奇数再乘一个 a
  • 代码短,逻辑清晰
  • 深度 O(log b),几乎不会爆栈
  • 常数略大,函数调用有开销
📉 迭代写法(推荐)
  • 思路:按位扫描二进制指数
  • 无递归开销,常数更小
  • 可以顺手处理"边读边取模"的超大指数
  • 竞赛首选,模板固定好背
3逆元的定义与存在条件 核心

定义:设 gcd(a, m) = 1。若整数 x 满足 a·x ≡ 1 (mod m),则称 x 为 a 在模 m 下的乘法逆元,记作 a⁻¹ 或 inv(a)。

🔍 为什么必须 gcd(a, m) = 1?

必要性:假设存在 x 使 a·x ≡ 1 (mod m),即 a·x = 1 + k·m。设 d = gcd(a, m),则 d | a 且 d | m,于是 d | (a·x - k·m) = 1,故 d = 1。

充分性:由裴蜀定理,gcd(a, m) = 1 意味着存在整数 u, v 使 a·u + m·v = 1,两边模 m 即得 a·u ≡ 1 (mod m),u 就是逆元。

唯一性:若 x, y 都是逆元,则 a(x - y) ≡ 0 (mod m)。由 gcd(a, m) = 1 可约去 a,得 x ≡ y (mod m)。所以逆元在模 m 下唯一,我们通常取 0 到 m-1 之间的那个作为答案。

模数 m元素 agcd(a, m)逆元是否存在逆元值
7(素数)31✔ 存在5(3×5=15≡1)
7(素数)51✔ 存在3(5×3=15≡1)
6(合数)51✔ 存在5(5×5=25≡1)
6(合数)22✘ 不存在—
1233✘ 不存在—
1251✔ 存在5(5×5=25≡1)
🔑 判定口诀

看模数是不是素数 → 是素数则除 0 以外统统有逆元;
模数是合数 → 只能和它互质的数才有逆元,否则直接判无解。

4三种求逆元方法 高频考点

同一个逆元有不止一条路可以走,选哪条完全取决于模数是不是素数、要算一个还是要算一批。

1
🌾
费马小定理

p 为素数时 a⁻¹ ≡ a^(p-2)

2
🧮
扩展欧几里得

解 a·x + m·y = 1,任意互质都行

3
📋
线性递推打表

inv[i] = -(p/i)·inv[p%i] mod p

🌾 方法一:费马小定理(要求 p 为素数)

由费马小定理 a^(p-1) ≡ 1 (mod p)(gcd(a,p)=1),两边同乘 a⁻¹ 得 a⁻¹ ≡ a^(p-2) (mod p)。

所以只要用快速幂算一次 a^(p-2) mod p 就得到了逆元,复杂度 O(log p)。代码极短,是竞赛中最常用的方式。

局限:p 必须是素数,且 data 必须保证 a 不被 p 整除。

🧮 方法二:扩展欧几里得(通用)

要求 a·x ≡ 1 (mod m),等价于求整数解 a·x + m·y = 1。由于 gcd(a, m) = 1,裴蜀定理保证有解,扩展欧几里得能直接递推出 x。

求完把 x 调整到 [0, m) 内:x = (x % m + m) % m。

优势:模数可以是任意合数,只要 gcd(a, m) = 1 就行,适用范围最广。复杂度同样是 O(log m)。

📋 方法三:线性递推打表(求 1..n 的全部逆元)

若需要 1 到 n 每个数的逆元(例如预处理阶乘逆元求组合数),逐个用费马小定理要 O(n log p),而递推可以做到 O(n)。推导如下:

设 p = k·i + r,其中 k = ⌊p / i⌋,r = p mod i,则 k·i + r ≡ 0 (mod p)。

两边乘 i⁻¹ · r⁻¹:k·r⁻¹ + i⁻¹ ≡ 0 (mod p),于是

inv[i] = −⌊p / i⌋ × inv[p mod i]  (mod p)

注意 p mod i < i,所以递推时用到的值一定已经算好了,从 i = 2 推到 n 即可。代码中把负号写进模数里:inv[i] = (p - p / i) * inv[p % i] % p。

下面用 p = 7 手工推一遍,看看表格是怎么被"填"出来的:

i=1
i=2
i=3
i=4
i=5
i=6
1
4
5
2
3
6

上排为下标 i,下排为 inv[i](模 7)。逐个验证:3×5 = 15 ≡ 1、4×2 = 8 ≡ 1、6×6 = 36 ≡ 1 ✓

i⌊7/i⌋7 mod i递推式inv[i]验算 i × inv[i] mod 7
1——边界赋值11
231−(3)×inv[1] = −342×4 = 8 ≡ 1
321−(2)×inv[1] = −253×5 = 15 ≡ 1
413−(1)×inv[3] = −524×2 = 8 ≡ 1
512−(1)×inv[2] = −435×3 = 15 ≡ 1
611−(1)×inv[1] = −166×6 = 36 ≡ 1
✅ 该选哪种方法

只求一个逆元 + 模数是素数 → 费马小定理(代码最短)

只求一个逆元 + 模数不确定 → 扩展欧几里得(最稳)

求 1..n 全部逆元 → 线性递推,O(n)

❌ 常见误用

模数是合数却用费马小定理 —— 结论根本不成立。

a 与 p 不互质时仍强行求逆元 —— 逆元不存在,程序必然输出错误值。

只求一个数的逆元却写 O(n) 打表 —— 把 n 当 10⁹ 时直接 MLE/TLE。

5逆元与分数取模 实战武器

逆元最大的用途是让"分数"进入模运算体系。公式只有一条:

🔑 分数取模公式

(a / b) mod p  =  (a mod p) × inv(b) mod p

使用前提:gcd(b, p) = 1。竞赛中绝大多数情况 p 是大素数,此时只要 b 不是 p 的倍数就一定能用。

🔍 为什么这个公式是合理的?

在有理数范围内,a / b = a × b⁻¹。模运算里我们没办法表示"真正的分数",但可以保证一件事:如果某个整数 t 满足 t·b ≡ a (mod p),那么 t 就应该被当作 a/b 的模 p 代表元。

取 t = a × inv(b),则 t·b = a × inv(b) × b = a × (b × inv(b)) ≡ a × 1 = a (mod p),完全符合要求,因此公式成立。

直观理解:模世界里的"除以 b"= "乘以 b 的倒数",这个倒数就是逆元。

📊 分数取模实例:求 (5 / 3) mod 7
13⁻¹ mod 7 = 5
25 × 5 = 25
325 mod 7 = 4
验证:4 × 3 = 12 ≡ 5 (mod 7) ✓ 所以 (5/3) mod 7 = 4
🎬 交互演示:分数取模 (5 / 3) mod 7 的完整流程
1目标:找一个 0 到 6 之间的整数 t,使 t × 3 ≡ 5 (mod 7) —— 这个 t 就是"5/3 在模 7 世界里的化身"。
2第一步:求 3 的逆元。逐个试乘:3×1=3、3×2=6、3×3=9≡2、3×4=12≡5、3×5=15≡1 → inv(3) = 5。
3第二步:套公式 t = a × inv(b) mod p = 5 × 5 mod 7 = 25 mod 7。
4第三步:25 mod 7 = 4,所以 (5/3) mod 7 = 4。
5验证:4 × 3 = 12 ≡ 5 (mod 7) ✓ —— 4 乘回分母正好还原出分子。
6反面教材:若直接做整数除法 (5 mod 7) / (3 mod 7) = 5 / 3 = 1,而 1×3 = 3 ≢ 5,结果完全错误 —— 除法必须换成乘逆元。
💡 记忆链条:见分数 → 求分母逆元 → 乘分子 → 取模 → 乘回去验算。
真实分数模 7 下的表示做法验证
1 / 241 × inv(2) = 1 × 44 × 2 = 8 ≡ 1
1 / 351 × inv(3) = 1 × 55 × 3 = 15 ≡ 1
3 / 463 × inv(4) = 3 × 26 × 4 = 24 ≡ 3
5 / 345 × inv(3) = 5 × 54 × 3 = 12 ≡ 5
易混点:(a / b) mod p 与 (a mod p) / (b mod p) 完全不是一回事。前者是"先除后取模",后者是"取模之后做整数除法",两者没有任何关系。考试里看到"分数取模"四个字,第一反应必须是求逆元。
案例案例1:P1226【模板】快速幂||取余运算 必刷

题目:给定三个整数 a, b, p,求 a^b mod p。数据范围:0 ≤ a ≤ 10⁹,0 ≤ b ≤ 10⁹,1 ≤ p ≤ 10⁹。

输出格式为 a^b mod p=s,其中 a、b、p 都是输入的原值,s 是答案。

🔍 分析过程

第1步:判断复杂度要求 — b 高达 10⁹,暴力循环 b 次必然 TLE。需要使用 O(log b) 的快速幂。

第2步:处理底数 — 先令 base = a % p,避免后面的平方运算一上来就溢出。

第3步:初始化答案 — res = 1 % p 而不是 res = 1。因为当 p = 1 时,正确答案是 0(任何数模 1 都是 0),写成 1 就错了。

第4步:循环主体 — exp & 1 判断末位是否为 1,是则把 base 乘进 res;然后 base 自平方;最后 exp 右移一位。

第5步:数据类型 — res 与 base 都在 0 到 10⁹ 之间,相乘最大 10¹⁸,接近 long long 上限但不会溢出;若用 int 则必然溢出。

复杂度:O(log b) 时间,O(1) 额外空间。

C++P1226 快速幂 — 完整代码
#include <bits/stdc++.h>
using namespace std;

long long a, b, p;

// 计算 base^exp mod p,迭代实现,O(log exp)
long long qpow(long long base, long long exp) {
    long long res = 1 % p;   // p = 1 时答案必须是 0
    base %= p;              // 底数先取模,防止平方时溢出
    while (exp > 0) {
        if (exp & 1)                // 末位为 1,把当前权重乘进答案
            res = res * base % p;
        base = base * base % p;     // 权重升级:(a^(2^i))^2 = a^(2^(i+1))
        exp >>= 1;                  // 指数右移一位
    }
    return res;
}

int main() {
    scanf("%lld %lld %lld", &a, &b, &p);
    printf("%lld^%lld mod %lld=%lld\n", a, b, p, qpow(a, b));
    return 0;
}
🔍 代码逐段解析 · P1226 快速幂
1全局 long long a, b, p:三个数都可达 10⁹,必须 long long;放全局是因为 qpow 里也要用 p。
2res = 1 % p:乘法单位元初值。写成 1 的话 p = 1 时直接错,这是本题最大的坑。
3base %= p:底数先取模。若 a = 10⁹ 且不取模,第一轮 base*base 就是 10¹⁸,贴着 long long 上限太危险。
4if (exp & 1) res = res * base % p:看指数二进制末位,为 1 就把当前权重乘进答案,随即取模。
5base = base * base % p:权重升级 a¹→a²→a⁴→a⁸…,每轮固定执行一次(哪怕这一位是 0)。
6exp >>= 1:指数右移丢掉已处理的末位。b ≤ 10⁹ 时最多 30 轮就归零。
7printf 输出:格式串里的 a、b、p 是输入原值,只有最后的 s 是取模结果,别把取模后的 a 也打出来。
⚠️ 易错点:① res 写成 1 而非 1 % p;② 中间乘积用 int 导致溢出;③ 忘记对 base 预先取模。
运行结果
输入: 2 10 9
输出: 2^10 mod 9=7
(验算:2^10 = 1024,1024 mod 9 = 7)

输入: 3 13 100
输出: 3^13 mod 100=23
(验算:3^13 = 1594323,末两位为 23)

输入: 5 0 1
输出: 5^0 mod 1=0
(任何数模 1 都是 0,这就是 res 要写 1 % p 的原因)

💡 逐段解析

为什么 res 要初始化为 1 % p?乘法单位元是 1,但 p = 1 时整个模世界只剩一个元素 0,单位元也是 0,所以必须写成 1 % p 才能兼容边界。

为什么 base 要提前取模?若 a = 10⁹、p = 10⁹,第一轮 base = base * base 就是 10¹⁸,虽然没超 long long 上限但已经很危险;提前取模后 base 始终小于 p,乘积不会超过 10¹⁸。

循环几轮?b ≤ 10⁹ 时最多 30 轮,b ≤ 10¹⁸ 时最多 60 轮,这也是"六十次乘法算十亿次方"这句口诀的由来。

案例案例2:P3811【模板】乘法逆元 O(n) 打表

题目:给定 n 与素数 p,求 1 到 n 中每个整数在模 p 意义下的乘法逆元,每个占一行输出。数据范围:1 ≤ n ≤ 3×10⁶,n < p < 2×10³¹,p 为素数。

🔍 分析过程

第1步:识别需求 — 要的是一串逆元,不是单个。若对每个 i 都用费马小定理,复杂度 O(n log p),n = 3×10⁶ 时约有 9×10⁷ 次乘法,勉强但不够稳(且常数大)。

第2步:选择线性递推 — 用公式 inv[i] = (p - p / i) * inv[p % i] % p,每个 i 只需一次除法、一次乘法、一次取模,总复杂度 O(n)。

第3步:初始化边界 — inv[1] = 1(1 的逆元是它自己),循环从 i = 2 开始。

第4步:注意 1-indexed — 公式里 p % i 严格小于 i,所以依赖项已经算好,正向递推即可。

第5步:输出效率 — 3×10⁶ 行输出必须用 printf 或关掉同步的 cout,否则会因 I/O 过慢超时。

C++P3811 线性求 1..n 的逆元 — 完整代码
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 3000005;
long long inv[MAXN];

int main() {
    long long n, p;
    scanf("%lld %lld", &n, &p);

    inv[1] = 1;                    // 边界:1 的逆元是 1
    for (int i = 2; i <= n; i++)
        // inv[i] = -(p/i) * inv[p%i] mod p,把负号并进模数写成 (p - p/i)
        inv[i] = (p - p / i) * inv[p % i] % p;

    for (int i = 1; i <= n; i++)
        printf("%lld\n", inv[i]);
    return 0;
}
运行结果
输入: 10 13
输出: 1
      7
      9
      10
      8
      11
      2
      5
      3
      4

验算(模 13):
  2 × 7  = 14 ≡ 1
  3 × 9  = 27 ≡ 1
  4 × 10 = 40 ≡ 1
  5 × 8  = 40 ≡ 1
  6 × 11 = 66 ≡ 1
  7 × 2  = 14 ≡ 1
  8 × 5  = 40 ≡ 1
  9 × 3  = 27 ≡ 1
 10 × 4  = 40 ≡ 1
全部成立 ✓

💡 为什么这个递推是对的?

取 p = k·i + r(k = p/i,r = p%i),两边模 p 得 k·i + r ≡ 0。

同乘 inv(i)·inv(r):k·inv(r) + inv(i) ≡ 0,移项即 inv(i) ≡ -k·inv(r)。

关键点是 r = p % i < i,所以计算 inv(i) 时 inv(r) 一定已经算过了 —— 这就是"线性"的来源。

案例案例3:P2613【模板】有理数取余 综合

题目:给出一个有理数 c = a / b,求 c mod 19260817 的值。其中 a、b 都是不超过 10^10001 的非负整数,需要按字符串读入。若 b 在模 19260817 下为 0(逆元不存在),输出 Angry!。

🔍 分析过程

第1步:解决超大输入 — a、b 有上万位,无法读进 long long。用字符串读入,边读边做 x = (x * 10 + c - '0') % MOD,把 a、b 各自"折叠"成一个模 19260817 的小整数。

第2步:特判无解 — 若 b 折叠后为 0,说明 b 是 19260817 的倍数,逆元不存在,按题意输出 Angry! 并结束。

第3步:求逆元 — 19260817 是素数,本可以用费马小定理;但为了同时演示扩展欧几里得的用法,本题用 exgcd 求 b 的逆元。

第4步:调整到正整数 — exgcd 得到的 x 可能在负数区间,必须写 x = (x % MOD + MOD) % MOD。

第5步:输出答案 — a * x % MOD,注意两个乘数都小于 19260817,乘积约 3.7×10¹⁴,long long 安全。

复杂度:读入 O(位数),求逆元 O(log MOD),总时间可以忽略不计。

C++P2613 有理数取余 — 完整代码
#include <bits/stdc++.h>
using namespace std;

const long long MOD = 19260817;

// 按字符串读入,边读边取模,返回被折叠后的余数
long long readMod() {
    string s; cin >> s;
    long long x = 0;
    for (char c : s)
        x = (x * 10 + (c - '0')) % MOD;
    return x;
}

// 扩展欧几里得:求 a*x + b*y = 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;
}

int main() {
    long long a = readMod();
    long long b = readMod();

    if (b == 0) {              // b 是 MOD 的倍数,逆元不存在
        cout << "Angry!" << endl;
        return 0;
    }

    long long x, y;
    exgcd(b, MOD, x, y);           // 解 b*x + MOD*y = 1
    x = (x % MOD + MOD) % MOD;     // 调整到 [0, MOD) 区间

    cout << a * x % MOD << endl;    // (a/b) mod p = a * inv(b) mod p
    return 0;
}
🔍 代码逐段解析 · P2613 有理数取余
1readMod():按字符串读入,逐位做 x = (x*10 + (c-'0')) % MOD,把上万位的大数"折叠"成 [0, MOD) 的余数 —— 高精度取模的通用套路。
2exgcd 递归:b == 0 时返回 a 并设 x=1, y=0;否则递归解 (b, a%b),返回时交换传引用 exgcd(b, a%b, y, x),再回代 y -= a / b * x。
3特判 if (b == 0):b 折叠后为 0 说明 b 是 19260817 的倍数,逆元不存在,按题意输出 Angry! 并结束。
4exgcd(b, MOD, x, y):解 b·x + MOD·y = 1,由于 gcd(b, MOD) = 1,x 就是 b 的逆元。
5x = (x % MOD + MOD) % MOD:exgcd 返回的 x 可能是负数,必须调整到 [0, MOD),否则乘出来的答案是负数或错值。
6cout << a * x % MOD:分数取模公式落地。两个乘数都小于 MOD,乘积约 3.7×10¹⁴,long long 安全。
⚠️ 易错点:① 不判 b==0 直接 exgcd,gcd ≠ 1 时解根本不是逆元;② 漏掉负数调整;③ readMod 忘记每步取模导致溢出。
运行结果
输入: 5
      3
输出: 12840545
(验算:12840545 × 3 = 38521635
   38521635 mod 19260817 = 5 ✓)

输入: 19260817
      1
输出: 0
(a 折叠后为 0,答案自然是 0)

输入: 7
      19260817
输出: Angry!
(b 折叠后为 0,逆元不存在)

输入: 1000000000000000000000
      2
输出: 9624560
(超大数被边读边取模压缩成小整数后正常运算)

💡 这道题的三层考点

第一层:超大整数读入技巧 —— 任何超过 long long 的输入都可以"边读边取模"压缩,这是高精度取模的通用套路。

第二层:逆元存在性判断 —— 必须先检查 b 折叠后是否为 0,否则 exgcd 会返回 gcd ≠ 1 而无法求解。

第三层:分数取模公式的正确落地 —— 先求逆元,再乘起来取模,顺序不能颠倒。

模块 03

实践应用

完整题目 + 样例 + 多角度分析 + 代码 + 运行结果。

题1快速幂取模(两种方法对比)
题1模板题方法对比

幂运算

题目背景:取模意义下的幂运算是一切数论算法的常数因子来源,必须随手写对。

输入格式:一行三个整数 a, b, p。

输出格式:一行一个整数,表示 a^b mod p。

输入样例:3 2000000000 1000000007

输出样例:693053420

数据范围:0 ≤ a, b ≤ 10⁹,1 ≤ p ≤ 10⁹。时间限制 1s,空间限制 256MB。

方法一:暴力循环 超时

循环 b 次做 res = res * a % p。复杂度 O(b),b = 2×10⁹ 时约需 2×10⁹ 次操作,远超 1s 能承受的 10⁸ 量级,必然 TLE。

方法二:二进制快速幂 正解

按二进制分解指数,复杂度 O(log b) ≈ 31 次循环。b = 2×10⁹ < 2³¹,只需 31 轮即可完成。

结论:同样的输入规模,暴力与快速幂的差距可以达到 6000 万倍,这就是"能不能过题"的分界线。

C++快速幂 — 完整代码(含两种写法)
#include <bits/stdc++.h>
using namespace std;

long long p;

// 方法一:暴力,O(b),仅用于对照
long long slowPow(long long a, long long b) {
    long long res = 1 % p;
    for (long long i = 1; i <= b; i++) res = res * (a % p) % p;
    return res;
}

// 方法二:二进制快速幂,O(log b)
long long qpow(long long base, long long exp) {
    long long res = 1 % p;
    base %= p;
    while (exp) {
        if (exp & 1) res = res * base % p;
        base = base * base % p;
        exp >>= 1;
    }
    return res;
}

int main() {
    long long a, b;
    cin >> a >> b >> p;
    cout << qpow(a, b) << endl;
    return 0;
}

🔍 代码逐段解析 · 快速幂两写法对照

第 1~3 行 —— 头文件与全局 long long p:slowPow 和 qpow 都要用模数,放全局免传参。

slowPow —— 暴力版:res 从 1 % p 起步,循环 b 次乘 (a % p),O(b)。b = 2×10⁹ 时约 2×10⁹ 次乘法,仅为对照,实际必 TLE。

qpow —— 迭代快速幂:先 base %= p,随后 while 循环"看末位(exp&1)→ 乘进答案 → 底数平方 → 指数右移",O(log b)。

main —— 读入后直接输出 qpow(a, b),一行核心调用。

易错点 —— ① res 初值必须写 1 % p;② 中间乘积若用 int,10⁹×10⁹ 直接溢出;③ 暴力版里 res * (a % p) 也必须全程 long long。

运行结果
输入: 3 2000000000 1000000007
输出: 863354199
(31 轮循环,运行时间 < 1ms;同样的输入用暴力需要约 2×10⁹ 次乘法)

💡 进一步思考

如果 b 有 1000 位(超过 long long),快速幂还能用吗?可以 —— 把 b 当字符串读入,边读边对 p-1 取模做欧拉降幂,或者直接按字符串的每一位"十进制快速幂"(每次做 10 次平方)。这是下一讲费马小定理的直接应用。

题2求最小正整数逆元
题2模板题两种方法

同余方程的最小正整数解

题目背景:给定 a 与 m(gcd(a, m) = 1),求 a 在模 m 下的最小正整数逆元。

输入格式:一行两个整数 a, m。

输出格式:一行一个整数 x,满足 a·x ≡ 1 (mod m) 且 1 ≤ x < m。

输入样例:3 11

输出样例:4

数据范围:1 ≤ a < m ≤ 10⁹,gcd(a, m) = 1。时间限制 1s,空间限制 256MB。

方法一:扩展欧几里得 通用

把 a·x ≡ 1 (mod m) 改写成 a·x + m·y = 1,直接调用 exgcd 得到 x,再做 (x % m + m) % m 调整。适用于任意模数(不要求素数),O(log m)。

方法二:费马小定理 仅素数

若 m 是素数,直接输出 qpow(a, m - 2)。代码只有一行调用,但必须确认 m 是素数,否则结果无意义。

选择建议:题目没有明说模数是素数时,一律用 exgcd,最保险。

C++求最小正逆元 — 完整代码
#include <bits/stdc++.h>
using namespace std;

// 扩展欧几里得:返回 gcd(a,b),并带回 a*x + b*y = 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;
}

int main() {
    long long a, m;
    cin >> a >> m;

    long long x, y;
    long long d = exgcd(a, m, x, y);

    if (d != 1) {                    // gcd 不为 1,逆元不存在
        cout << "No Solution" << endl;
        return 0;
    }
    x = (x % m + m) % m;             // 调整到最小正整数解
    cout << x << endl;
    return 0;
}

🔍 代码逐段解析 · exgcd 求最小正逆元

exgcd 函数 —— 递归边界 if (b == 0) 时 x=1, y=0;否则递归解 (b, a%b),返回后回代 y -= a / b * x。递归深度 O(log m)。

main 读入与求解 —— d = exgcd(a, m, x, y) 同时拿到 gcd 和一组特解。

无解判断 —— if (d != 1) 时逆元不存在,输出 No Solution。不判直接用 x 是高频错误。

归一化 —— x = (x % m + m) % m:exgcd 给出的 x 可能是负数或超过 m,先 % m 再 + m 再 % m,保证落在最小正整数解。

易错点 —— ① 漏掉负数调整输出负逆元;② 误以为费马小定理适用于任意 m(本题 m 只是"与 a 互质"的任意数)。

运行结果
输入: 3 11
输出: 4
(验算:3 × 4 = 12 ≡ 1 (mod 11) ✓)

输入: 5 12
输出: 5
(验算:5 × 5 = 25 ≡ 1 (mod 12) ✓ 模数 12 是合数,exgcd 照样能用)

输入: 4 6
输出: No Solution
(gcd(4,6) = 2 ≠ 1,逆元不存在)

💡 为什么能用 exgcd 求逆元?

exgcd 解的是 a·x + m·y = gcd(a, m)。当 gcd = 1 时,这个方程就是 a·x + m·y = 1,两边模 m 得 a·x ≡ 1 (mod m) —— 恰好就是逆元的定义式。

这也解释了为什么逆元存在当且仅当 gcd(a, m) = 1:exgcd 只能解出右端为 gcd 的方程,右端为 1 就必须 gcd = 1。

题3组合数取模(阶乘 + 逆元预处理)
题3综合应用

组合数 C(n, m) mod p

题目背景:组合数是计数类题目的骨架,而组合数公式里带着一个除法 —— 这正是逆元的用武之地。

输入格式:第一行两个整数 n, q;接下来 q 行每行两个整数 nᵢ, mᵢ。

输出格式:共 q 行,每行一个整数表示 C(nᵢ, mᵢ) mod p。

输入样例:10 3
10 3
10 5
8 0

输出样例:120
252
1

数据范围:1 ≤ n ≤ 10⁶,1 ≤ q ≤ 10⁵,p = 10⁹ + 7(素数)。时间限制 1s,空间限制 256MB。

方法一:直接套公式逐个求逆元 偏慢

每次查询都用费马小定理求 n!、m!、(n-m)! 的逆元,单次 O(log p),总复杂度 O(q log p)。q = 10⁵ 时约 3×10⁶ 次乘法,能过但不优雅。

方法二:预处理阶乘与阶乘逆元 推荐

先 O(n) 打出 fac[i] = i!,再用一次费马小定理求 invFac[n] = inv(fac[n]),然后倒着递推:

invFac[i-1] = invFac[i] × i mod p

因为 invFac[i-1] = 1/(i-1)! = i / i! = i × invFac[i]。预处理 O(n),单次查询 O(1),总复杂度 O(n + q),完美。

公式:C(n, m) = fac[n] × invFac[m] × invFac[n-m] mod p(m > n 时答案为 0)。

C++组合数取模 — 完整代码
#include <bits/stdc++.h>
using namespace std;

const int MAXN = 1000005;
const long long MOD = 1000000007;
long long fac[MAXN], invFac[MAXN];

long long qpow(long long base, long long exp) {
    long long res = 1;
    base %= MOD;
    while (exp) {
        if (exp & 1) res = res * base % MOD;
        base = base * base % MOD;
        exp >>= 1;
    }
    return res;
}

int main() {
    int n, q;
    scanf("%d %d", &n, &q);

    // 1. 预处理阶乘
    fac[0] = 1;
    for (int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i % MOD;

    // 2. 用费马小定理求 n! 的逆元(MOD 是素数)
    invFac[n] = qpow(fac[n], MOD - 2);

    // 3. 倒推所有阶乘逆元:invFac[i-1] = invFac[i] * i
    for (int i = n; i >= 1; i--) invFac[i - 1] = invFac[i] * i % MOD;

    while (q--) {
        int a, b;
        scanf("%d %d", &a, &b);
        if (b < 0 || b > a) { puts("0"); continue; }
        long long ans = fac[a] * invFac[b] % MOD * invFac[a - b] % MOD;
        printf("%lld\n", ans);
    }
    return 0;
}
运行结果
输入: 10 3
      10 3
      10 5
      8 0
输出: 120
      252
      1

验算:C(10,3) = 10!/(3!·7!) = (10×9×8)/6 = 120 ✓
      C(10,5) = 252 ✓
      C(8,0) = 1 ✓

💡 提炼成一句口诀

"组合数 = 阶乘相乘再乘逆元"。整个套路固定为:打阶乘表 → 求最末一个阶乘逆元 → 倒推全部阶乘逆元 → O(1) 回答查询。

这个模板在 CSP-S 提高组里出现频率极高,凡是"方案数模 10⁹+7"的题目,几乎都可以先写出这套预处理。

模块 04

总结提升

①核心要点与万能模板
知识板块必须记住的核心复杂度
同余定义a ≡ b (mod m) ⟺ m | (a−b) ⟺ 存在 k 使 a = b + km—
同余运算加减乘保持同余,除法必须转成乘逆元—
负数取模写成 (x % m + m) % mO(1)
快速幂末位为 1 乘进答案,底数平方,指数右移O(log b)
逆元存在性存在 ⟺ gcd(a, m) = 1;模 m 下唯一—
费马小定理法p 为素数时 a⁻¹ = a^(p−2) mod pO(log p)
扩展欧几里得法解 a·x + m·y = 1,适用于任意模数O(log m)
线性递推法inv[i] = (p − p/i) × inv[p % i] % pO(n)
分数取模(a/b) mod p = a × inv(b) mod pO(log p)
组合数取模C(n,m) = fac[n] × invFac[m] × invFac[n−m]O(n) 预处理 + O(1) 查询
C++万能模板:快速幂 + exgcd + 逆元三件套
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

/* ===== 1. 快速幂:a^b mod p ===== */
ll qpow(ll base, ll exp, ll p) {
    ll res = 1 % p;
    base %= p;
    while (exp) {
        if (exp & 1) res = res * base % p;
        base = base * base % p;
        exp >>= 1;
    }
    return res;
}

/* ===== 2. 扩展欧几里得:a*x + b*y = gcd(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, y, x);
    y -= a / b * x;
    return d;
}

/* ===== 3. 单个逆元(通用,任意模数) ===== */
ll inv(ll a, ll m) {
    ll x, y;
    exgcd(a, m, x, y);
    return (x % m + m) % m;
}

/* ===== 4. 单个逆元(仅 p 为素数,更短更快) ===== */
ll invPrime(ll a, ll p) { return qpow(a, p - 2, p); }

/* ===== 5. 线性求 1..n 的全部逆元 ===== */
const int MAXN = 3000005;
ll invTab[MAXN];
void buildInv(ll n, ll p) {
    invTab[1] = 1;
    for (int i = 2; i <= n; i++)
        invTab[i] = (p - p / i) * invTab[p % i] % p;
}

🔍 代码逐段解析 · 逆元三件套万能模板

qpow(base, exp, p) —— p 作参数的通用版;res = 1 % p 兼容 p = 1,base 先取模。

exgcd —— 递归 + 回代,返回 gcd(a,b),带回一组解;任何调用方都要检查返回值。

inv(a, m) —— 通用逆元:调 exgcd 后 (x % m + m) % m,任意模数(只要互质)都能用。

invPrime(a, p) —— 素数模专用:qpow(a, p - 2, p),一行搞定,但前提 p 必须是素数。

buildInv(n, p) —— 线性打表:invTab[1] = 1 起步,公式 (p - p/i) * invTab[p%i] % p,O(n) 求全部逆元。

易错点 —— ① 场景混用:模数是合数时误调 invPrime;② buildInv 要求 1 ≤ i < p,n 超过 p 时公式失效。

🔑 考场三步走

① 看到"取模 + 除法" → 立即想到逆元;
② 确认模数是否素数 + 除数是否互质 → 决定用哪种求法;
③ 所有中间结果用 long long,负数结果一律 (x % m + m) % m。

②拓展案例
拓展1:等比数列求和(分治 vs 逆元公式)

问题:给定 a, n, p(p 为素数,a mod p ≠ 1),求 a + a² + a³ + … + aⁿ mod p,n ≤ 10¹⁸。

方法一:分治 无需逆元

记 S(n) = a + a² + … + aⁿ。若 n 为偶数,则前半段与后半段只差一个 a^(n/2) 因子:

S(n) = S(n/2) × (1 + a^(n/2))

若 n 为奇数,则 S(n) = S(n−1) + aⁿ。递归深度 O(log n),每层带一次快速幂,总复杂度 O(log² n)。

方法二:逆元公式 O(log n)

由等比数列求和公式,分子分母同乘逆元:

S(n) = (a^(n+1) − a) × inv(a − 1) mod p

只需一次快速幂加一次求逆元,复杂度 O(log n)。但要求 a ≢ 1 (mod p),否则 a − 1 无逆元,必须退回分治法。

C++等比数列求和 — 两种实现
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const long long MOD = 1000000007;

ll qpow(ll base, ll exp) {
    ll res = 1; base %= MOD;
    while (exp) {
        if (exp & 1) res = res * base % MOD;
        base = base * base % MOD; exp >>= 1;
    }
    return res;
}

// 方法一:分治,O(log^2 n),不需要逆元
ll sumPow(ll a, ll n) {
    if (n == 0) return 0;
    if (n & 1) return (sumPow(a, n - 1) + qpow(a, n)) % MOD;
    ll half = sumPow(a, n / 2);
    return half * (1 + qpow(a, n / 2)) % MOD;
}

// 方法二:逆元公式,O(log n),要求 a % MOD != 1
ll sumPowFast(ll a, ll n) {
    ll num = (qpow(a, n + 1) - a % MOD + MOD) % MOD;
    ll den = qpow((a - 1 + MOD) % MOD, MOD - 2);
    return num * den % MOD;
}

int main() {
    cout << sumPow(2, 10) << " " << sumPowFast(2, 10) << endl;
    cout << sumPow(3, 1000000000000000000LL) << endl;
    return 0;
}

🔍 代码逐段解析 · 等比数列求和两实现

qpow —— 标准快速幂,被两种方法共同调用。

sumPow(分治) —— n 为奇数先拆出一项 sumPow(a, n-1) + qpow(a, n);n 为偶数用 S(n/2) * (1 + a^(n/2))。递归深度 O(log n),每层一次快速幂,总 O(log²n),不依赖逆元。

sumPowFast(逆元公式) —— 分子 (qpow(a, n+1) - a % MOD + MOD) % MOD:先减后补 MOD 防负数;分母 qpow((a - 1 + MOD) % MOD, MOD - 2) 用费马小定理求 inv(a−1)。

易错点 —— ① a ≡ 1 (mod p) 时 a−1 无逆元,此函数必须先特判(答案为 (n+1) mod p);② 减法后忘记 + MOD 导致负数输出。

运行结果
2046 2046
(2 + 4 + ... + 1024 = 2046,两种方法一致 ✓)

983857628 占位
拓展2:矩阵快速幂求斐波那契第 n 项

问题:求 F(n) mod p,其中 F(1) = F(2) = 1,n ≤ 10¹⁸。

思路 快速幂的推广

把递推写成矩阵形式:

[F(n+1), F(n)]ᵀ = [[1,1],[1,0]] × [F(n), F(n−1)]ᵀ

于是 [F(n+1), F(n)] = [[1,1],[1,0]]^(n−1) × [F(2), F(1)]。矩阵乘法的"指数"同样是数,只是乘法变成了矩阵乘法 —— 快速幂的二进制分解逻辑一字不改就能复用。

复杂度:矩阵乘法常数是 2³ = 8 次标量乘法,总复杂度 O(8 · log n)。

C++矩阵快速幂 — 完整代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const long long MOD = 1000000007;

struct Mat {
    ll m[2][2];
    Mat() { memset(m, 0, sizeof(m)); }
};

Mat mul(Mat A, Mat B) {
    Mat C;
    for (int i = 0; i < 2; i++)
        for (int k = 0; k < 2; k++) {
            if (!A.m[i][k]) continue;
            for (int j = 0; j < 2; j++)
                C.m[i][j] = (C.m[i][j] + A.m[i][k] * B.m[k][j]) % MOD;
        }
    return C;
}

Mat mpow(Mat A, ll n) {
    Mat R;
    R.m[0][0] = R.m[1][1] = 1;  // 单位阵,相当于数字里的 1
    while (n) {
        if (n & 1) R = mul(R, A);
        A = mul(A, A);
        n >>= 1;
    }
    return R;
}

int main() {
    ll n; cin >> n;
    if (n <= 2) { cout << 1 << endl; return 0; }
    Mat A;
    A.m[0][0] = 1; A.m[0][1] = 1;
    A.m[1][0] = 1; A.m[1][1] = 0;
    Mat R = mpow(A, n - 1);
    cout << R.m[0][0] << endl;   // 左上角即 F(n)
    return 0;
}
运行结果
输入: 10
输出: 55

输入: 50
输出: 586268941
(F(50) = 12586269025,模 10^9+7 得 586268941)

输入: 1000000000000000000
输出: 209783453
(n = 10^18,只需约 60 轮矩阵乘法)
拓展3:指数有上万位怎么办 —— 十进制快速幂

问题:求 a^b mod p,其中 b 是长度可达 10⁵ 的十进制字符串。

方法一:十进制快速幂 绝对正确

把指数 b 看成十进制串,从高位到低位处理,每位做"乘 10 再补上这一位":

res ← res¹⁰ × a^(当前数字)

例如 b = "123":先处理 1 得 a¹;处理 2 时 (a¹)¹⁰ × a² = a¹²;处理 3 时 (a¹²)¹⁰ × a³ = a¹²³。正确性显然。

方法二:欧拉降幂 下一讲详解

由费马小定理,a^b ≡ a^(b mod (p−1)) (mod p)(当 gcd(a,p)=1)。于是只需把 b 这个巨大字符串边读边对 p−1 取模,压缩成小整数再快速幂。复杂度 O(len + log p),比方法一更快。

C++十进制快速幂 — 完整代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const long long MOD = 1000000007;

// 快速幂:a^n mod MOD
ll qpow(ll a, ll n) {
    ll res = 1; a %= MOD;
    while (n) {
        if (n & 1) res = res * a % MOD;
        a = a * a % MOD; n >>= 1;
    }
    return res;
}

// 指数以十进制字符串给出
ll powDec(ll a, const string &b) {
    ll res = 1;
    for (char c : b) {
        res = qpow(res, 10);                    // 指数"乘 10"
        res = res * qpow(a, c - '0') % MOD;      // 再补上这一位数字
    }
    return res;
}

int main() {
    ll a; string b;
    cin >> a >> b;
    cout << powDec(a, b) << endl;
    return 0;
}

🔍 代码逐段解析 · 十进制快速幂

qpow —— 标准快速幂,先 a %= MOD。

powDec 核心 —— 逐字符扫描指数串:每轮 res = qpow(res, 10) 相当于 res 的指数"乘 10",再 res = res * qpow(a, c-'0') % MOD 补上当前数字。循环不变量:res ≡ a^(已处理前缀) (mod MOD)。

main —— a 用 ll 读入,b 用 string 读入(长度可达 10⁵,绝不能转整数)。

易错点 —— ① 用 stoll 转换超长字符串直接溢出/抛异常;② 两位处理顺序颠倒(先补数字再乘 10 就变成从低位算了);③ qpow(res, 10) 里 res 未取模就传入(本实现 qpow 内部有 a %= MOD,安全)。

运行结果
输入: 2 1000
输出: 688423210
(验算:2^1000 mod 10^9+7 = 688423210 ✓)

输入: 3 123456789012345678901234567890
输出: 175859480
(指数 30 位,long long 装不下;字符串读入完美解决)

说明:十进制快速幂每位做 10 次平方 + 最多 4 次乘法,
     指数长度 10^5 时约 3×10^6 次乘法,完全可以在 1s 内跑完。
③思考与启发

💡 三个值得深挖的问题

思考1:为什么 (a − b) mod m 要写成 ((a % m − b % m) + m) % m?如果 m 本身就接近 10¹⁸,这种写法还安全吗?

查看参考答案

思路:C++ 的 % 是"截断取余",符号跟随被除数。a % m − b % m 的最小值约 −(m−1),直接取模会得到负数。

为什么要加 m:加上一个 m 后结果落在 [1, 2m−1] 区间,再取模就落回 [0, m−1],不会因为加了 m 而"多加一圈"。

安全性:当 m 接近 10¹⁸ 时,a % m − b % m + m 不会超过 2×10¹⁸,仍在 long long 上限 9.22×10¹⁸ 之内,所以是安全的。但如果写成 a % m + m 而 a % m 也可达 m 量级,同样没超 —— 关键是加一次 m 就够,不要加两次。

易错:写成 (x % m + 2*m) % m 或先取了 x % m 又对 x 本身加 m 再取模,容易在 m 极大时溢出。

思考2:如果 gcd(a, p) = 2(比如 p = 10、a = 4),那么 "a/2 mod 10" 有没有意义?

查看参考答案

结论:此时不能说"算出某个唯一值",但方程 a·x ≡ b (mod m) 仍可能有多个解。

分析:设 d = gcd(a, m)。方程 a·x ≡ b (mod m) 有解 ⟺ d | b,此时解恰好有 d 个,它们在模 m 下分别相隔 m/d。

例子:4x ≡ 2 (mod 10)。d = gcd(4,10) = 2,2 | 2 有解。枚举 x = 3 得 12 ≡ 2 ✓;x = 8 得 32 ≡ 2 ✓。所有解为 x ≡ 3, 8 (mod 10),共有 2 个。

工程含义:逆元存在时解唯一,可以直接用 inv(b);不互质时要么判无解,要么保留多个解。这就是很多题目要求"保证 b 与 p 互质"的原因。

易错:不判 gcd 直接调用 exgcd,得到的是 gcd ≠ 1,却把 x 当成逆元继续使用,结果当然是错的。

思考3:线性递推求逆元的公式里出现了 inv[p % i]。如果 p 是合数,这个递推还能用吗?

查看参考答案

不能无条件使用。递推的前提是 p % i 本身与 p 互质,否则 inv[p % i] 不存在,整个公式失效。

为什么 p 是素数时一定成立:p 为素数时,只要 1 ≤ i < p,就有 gcd(i, p) = 1,于是 p % i 也必然与 p 互质(因为 p % i 是 p 与 i 的线性组合),逆元一定存在。

正确做法:先在 [1, n] 中筛出与 p 互质的数,只对这部分下标做递推;或者直接对所有需要的数用费马小定理 / exgcd 单独求,虽然慢一些但一定正确。

易错:模数是合数(例如 10⁹ 或者几个素数之积)时照抄线性递推模板,恰好在递推路径上遇到不互质的项,输出全乱。

模块 05

学习检测

先找错,再背口诀 —— 把易错点消灭在考试之前。

易错1

快速幂的 res 初始化成 1

❌ 错误
long long res = 1;
当 p = 1 时,任何数模 1 都是 0,而这段代码会返回 1,第 5 个测试点必挂。
✅ 正确
long long res = 1 % p;
p = 1 时 res 自动变成 0,p > 1 时仍是 1,两种情形都正确。
口诀:答案初值 1 % p,p 等于 1 也不怕!
易错2

快速幂里用 int 相乘溢出

❌ 错误
int res = 1, base = a; 当 p 达到 10⁹ 时,res * base 最大约 10¹⁸,远超 int 的 2.1×10⁹ 上限,结果直接变成随机值。
✅ 正确
所有参与乘法取模的变量统一声明为 long long:long long res = 1 % p, base = a % p;
口诀:取模乘法用 long long,两个十亿相乘也不慌!
易错3

模数是合数却用费马小定理

❌ 错误
inv = qpow(a, m - 2, m); 当 m = 12、a = 5 时算出 5^(10) mod 12 = 1,看着像对,但当 m = 10、a = 3 时 gcd(3,10)=1 却算出 3^8 mod 10 = 1,而真实逆元是 7 —— 完全错。
✅ 正确
费马小定理 只在模数为素数时成立。模数不确定时一律用扩展欧几里得:exgcd(a, m, x, y); x = (x % m + m) % m;
口诀:素数用费马,合数用扩欧,拿不准就用扩欧!
易错4

线性递推公式漏掉负号 / 忘设边界

❌ 错误
inv[i] = (p / i) * inv[p % i] % p;
漏了负号,模 7 时算出 inv[2] = 3,而 2×3 = 6 ≢ 1。另外忘记 inv[1] = 1 会读到垃圾值。
✅ 正确
inv[1] = 1;
inv[i] = (p - p / i) * inv[p % i] % p;
把"负 k"写成"p − k",天然落在 [0, p) 内,不用再调整负数。
口诀:inv[1] 先设一,p 减商再乘 inv 余!
易错5

把分数取模写成两个余数相除

❌ 错误
ans = (a % p) / (b % p);
这是整数除法,和 (a/b) mod p 毫无关系。例如 (5/3) mod 7 真实答案是 4,这段代码输出 5/3 = 1。
✅ 正确
先求逆元再相乘:ans = (a % p) * inv(b) % p;
其中 inv(b) 用费马或 exgcd 求出,且必须先确认 gcd(b, p) = 1。
口诀:模下除法变乘法,先求逆元再相乘!
📋速查表
功能公式 / 代码复杂度使用场景
快速幂末位乘 → 平方 → 右移O(log b)一切大指数取模
逆元(素数模)inv = qpow(a, p−2, p)O(log p)p 为大素数
逆元(通用)exgcd(a, m, x, y) → xO(log m)模数任意
逆元(打表)inv[i] = (p−p/i)·inv[p%i]%pO(n)需 1..n 全部逆元
分数取模a × inv(b) % pO(log p)有理数比较 / 计数
阶乘逆元倒推invFac[i−1] = invFac[i] × iO(n)组合数取模
组数合取模fac[n]·invFac[m]·invFac[n−m]O(1)计数类题目
负数取模(x % m + m) % mO(1)减法运算后
📌笔记卡
必记概念
  • 同余 ⟺ 差被整除 ⟺ 余数相同
  • 加减乘保持,除法不保持
  • 逆元存在 ⟺ gcd(a, m) = 1
  • 逆元在模 m 下唯一
  • (a/b) mod p = a·inv(b) mod p
必背模板
  • 快速幂六步循环
  • exgcd 递归 + 回代
  • inv[i] = (p−p/i)·inv[p%i]%p
  • 阶乘逆元倒推
  • 字符串边读边取模
考场提醒
  • 先问"模数是素数吗"
  • 乘法一律 long long
  • res 初值写 1 % p
  • 减法后补 (x%m+m)%m
  • 大输出关同步或用 printf
课后练习

分层练习作业

A基础 / B提高 / C挑战,难度递增。C组后附 12 道洛谷原题。

🟢 A组 · 基础(4题)

A1编程题

快速幂:输入 a, b, p(a, b ≤ 10⁹,p ≤ 10⁹),输出 a^b mod p。

查看参考答案

思路:把指数 b 按二进制拆开,从低位到高位逐位处理:末位为 1 就把当前底数乘进答案,然后底数自平方、指数右移。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
int main() {
    ll a, b, p; cin >> a >> b >> p;
    ll res = 1 % p, base = a % p;
    while (b) {
        if (b & 1) res = res * base % p;
        base = base * base % p;
        b >>= 1;
    }
    cout << res << endl;
    return 0;
}

考点:二进制分解指数,O(log b) 完成幂运算。

易错:res 写成 1 而非 1 % p(p = 1 时错);用 int 存中间乘积导致溢出;忘记对底数先取模。

A2编程题

求单个逆元:给定素数 p 与 a(1 ≤ a < p ≤ 10⁹),输出 a 在模 p 下的逆元。

查看参考答案

思路:p 是素数,直接用费马小定理 inv(a) ≡ a^(p−2) (mod p),调用快速幂即可。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll a, p;
ll qpow(ll b, ll e) {
    ll r = 1 % p; b %= p;
    while (e) {
        if (e & 1) r = r * b % p;
        b = b * b % p; e >>= 1;
    }
    return r;
}
int main() {
    cin >> a >> p;
    cout << qpow(a, p - 2) << endl;
    return 0;
}

🔍 代码逐段解析

全局 a, p —— qpow 函数内要用 p 取模,放全局共享。

qpow(b, e) —— r = 1 % p; b %= p; 两个防御性初值都不可少。

main —— 输出 qpow(a, p - 2):指数是 p−2 而不是 p−1,这是费马小定理求逆元的关键。

易错点 —— ① 指数写成 p − 1(那算出的是 1);② p 不是素数时此法完全失效。

考点:费马小定理 + 快速幂的组合使用。

易错:把指数写成 p − 1(那是求 a^(p−1) ≡ 1,不是逆元);p 不是素数时这个做法完全失效。

A3编程题

线性求逆元:给定 n 与素数 p(n ≤ 10⁶),输出 1 到 n 每个数模 p 的逆元。

查看参考答案

思路:利用 inv[i] = (p − p/i) × inv[p % i] % p,从 2 递推到 n,总复杂度 O(n)。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 1000005;
ll inv[MAXN];
int main() {
    ll n, p; scanf("%lld %lld", &n, &p);
    inv[1] = 1;
    for (int i = 2; i <= n; i++)
        inv[i] = (p - p / i) * inv[p % i] % p;
    for (int i = 1; i <= n; i++) printf("%lld\n", inv[i]);
    return 0;
}

考点:线性递推求逆元的推导与实现。

易错:漏写 inv[1] = 1;公式漏负号(正确写法用 p − p/i 代替 −(p/i));p 是合数时该递推不保证成立。

A4编程题

分数取模:给定 a, b, p(p 为素数,1 ≤ b < p),求 (a/b) mod p。

查看参考答案

思路:先求 b 的逆元,再与 a 相乘取模:ans = a % p × inv(b) % p。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll p;
ll qpow(ll b, ll e) {
    ll r = 1 % p; b %= p;
    while (e) { if (e & 1) r = r * b % p; b = b * b % p; e >>= 1; }
    return r;
}
int main() {
    ll a, b; cin >> a >> b >> p;
    cout << (a % p) * qpow(b, p - 2) % p << endl;
    return 0;
}

🔍 代码逐段解析

qpow —— 快速幂求逆元专用(p 为素数,直接 a^(p−2))。

main 一行核心 —— (a % p) * qpow(b, p - 2) % p:先取模 → 乘逆元 → 再取模,正是分数取模公式。

易错点 —— ① 写成 (a % p) / (b % p) 整数除法;② a 未先取模导致乘积溢出。

考点:逆元的实际用途 —— 把除法搬进模世界。

易错:写成 (a % p) / (b % p)(这是整数除法,完全错误);忘记 b 与 p 必须互质。

🟡 B组 · 提高(4题)

B1组合数取模

给定 n ≤ 10⁶ 与 q 次询问,每次给出 m,输出 C(n, m) mod 10⁹+7。

查看参考答案

思路:预处理阶乘表与阶乘逆元表,单次查询 O(1)。阶乘逆元用"先求末项、再倒推"的方式,复杂度 O(n + q)。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 1000005;
const ll MOD = 1000000007;
ll fac[MAXN], invFac[MAXN];
ll qpow(ll b, ll e) {
    ll r = 1; b %= MOD;
    while (e) { if (e & 1) r = r * b % MOD; b = b * b % MOD; e >>= 1; }
    return r;
}
int main() {
    int n, q; scanf("%d %d", &n, &q);
    fac[0] = 1;
    for (int i = 1; i <= n; i++) fac[i] = fac[i - 1] * i % MOD;
    invFac[n] = qpow(fac[n], MOD - 2);
    for (int i = n; i >= 1; i--) invFac[i - 1] = invFac[i] * i % MOD;
    while (q--) {
        int m; scanf("%d", &m);
        if (m > n) { puts("0"); continue; }
        printf("%lld\n", fac[n] * invFac[m] % MOD * invFac[n - m] % MOD);
    }
    return 0;
}

考点:组合数公式中的除法 → 逆元 → 预处理阶乘逆元。

易错:忘记倒推 invFac[i−1] = invFac[i] * i,改成对每个 i 单独求逆元会退化到 O(n log p);m > n 时没判 0。

B2分治 + 快速幂

求 1 + a + a² + … + aⁿ mod p(n ≤ 10¹⁸,a ≢ 1 mod p)。

查看参考答案

方法一(分治):S(n) = S(n/2) × (1 + a^(n/2))(n 偶数),S(n) = S(n−1) + aⁿ(n 奇数),O(log² n),不需要逆元。

方法二(逆元公式,更优):S(n) = (a^(n+1) − 1) × inv(a − 1),O(log n)。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll P = 1000000007;
ll qpow(ll b, ll e) {
    ll r = 1 % P; b %= P;
    while (e) { if (e & 1) r = r * b % P; b = b * b % P; e >>= 1; }
    return r;
}
int main() {
    ll a, n; cin >> a >> n;
    ll num = (qpow(a, n + 1) - 1 + P) % P;
    ll den = qpow((a - 1 + P) % P, P - 2);
    cout << num * den % P << endl;
    return 0;
}

考点:等比数列求和的逆元写法;分治法作为不依赖逆元的备选。

易错:a ≡ 1 (mod p) 时 a − 1 无逆元,必须特判(此时答案为 (n+1) mod p);分子减 1 后忘记补 P 导致负数。

B3同余方程

解 ax ≡ b (mod m),输出 [0, m) 内的最小非负整数解;若无解输出 -1。

查看参考答案

思路:设 d = gcd(a, m)。方程有解 ⟺ d | b。有解时先解 a'x + m'y = 1(其中 a' = a/d,m' = m/d),得到 x₀,则最小非负解为 (x₀ × (b/d)) mod m'。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll exgcd(ll a, ll b, ll &x, ll &y) {
    if (!b) { x = 1; y = 0; return a; }
    ll d = exgcd(b, a % b, y, x);
    y -= a / b * x; return d;
}
int main() {
    ll a, b, m; cin >> a >> b >> m;
    ll x, y, d = exgcd(a, m, x, y);
    if (b % d) { cout << -1 << endl; return 0; }
    ll mod = m / d;
    x = (__int128)x * (b / d) % mod;
    cout << (x % mod + mod) % mod << endl;
    return 0;
}

🔍 代码逐段解析

exgcd —— 求出 gcd(a, m) 与特解 x, y。

可解性判断 —— if (b % d) 非零即无解输出 −1,必须放在求逆之前。

归一化 —— mod = m / d;x = (__int128)x * (b/d) % mod 用 __int128 防乘法溢出;最后 (x % mod + mod) % mod 落到最小非负解。

易错点 —— ① 对 m 而不是 m/d 取模(解的周期是 m/d);② x*(b/d) 直接相乘溢出。

考点:裴蜀定理的可解性判断 + exgcd 求特解 + 模 m/d 归一化。

易错:不判 d | b 就直接求逆元;归一化时对 m 取模而不是 m/d(解集周期是 m/d 不是 m);乘法溢出未用 __int128 或先取模。

B4超大指数

求 a^b mod p,其中 b 以十进制字符串给出,长度可达 10⁵。

查看参考答案

思路:从高位到低位扫描字符串,每读一位做 res ← res¹⁰ × a^(该位数字),本质上就是"十进制版的快速幂"。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll qpow(ll b, ll e, ll p) {
    ll r = 1 % p; b %= p;
    while (e) { if (e & 1) r = r * b % p; b = b * b % p; e >>= 1; }
    return r;
}
int main() {
    ll a, p; string b;
    cin >> a >> b >> p;
    ll res = 1 % p;
    for (char c : b) {
        res = qpow(res, 10, p);
        res = res * qpow(a, c - '0', p) % p;
    }
    cout << res << endl;
    return 0;
}

考点:大指数问题;若 a 与 p 互质,也可改用欧拉降幂(下一讲)。

易错:尝试用 stoll 把 b 转成整数(直接溢出/抛异常);忘记 res = 1 % p。

🔴 C组 · 挑战(4题)

C1初赛选择题

设 m 为正整数,下列结论中正确的个数是( )
① 若 a ≡ b (mod m),则 a² ≡ b² (mod m)
② 若 a² ≡ b² (mod m),则 a ≡ b (mod m)
③ 若 ac ≡ bc (mod m) 且 gcd(c, m) = 1,则 a ≡ b (mod m)
④ 若 ac ≡ bc (mod m),则必有 a ≡ b (mod m)

A. 1    B. 2    C. 3    D. 4

查看参考答案

答案:B(2 个)

①正确:乘法保持同余,a ≡ b 两边自乘即可。

②错误:反例 m = 8,a = 1,b = 3。1² = 1、3² = 9 ≡ 1,但 1 ≢ 3。

③正确:c 与 m 互质意味着 c 有逆元,两边同乘 inv(c) 即可约掉,这正是"除法不保持同余"的例外条件。

④错误:反例 m = 6,c = 2,a = 1,b = 4。ac = 2、bc = 8 ≡ 2 (mod 6),但 1 ≢ 4 (mod 6)。

考点:同余的乘方保持性、约去律成立的条件(必须 gcd(c, m) = 1)。

易错:误以为"两边同除一个数总是合法",忘记需要互质这个前提。

C2综合编程

矩阵快速幂:求斐波那契数列第 n 项 mod 10⁹+7,n ≤ 10¹⁸。

查看参考答案

思路:递推式写成矩阵 [[1,1],[1,0]],则 [[1,1],[1,0]]^(n−1) 的左上角就是 F(n)。用快速幂的二进制分解逻辑对矩阵做同样的操作。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll P = 1000000007;
struct Mat { ll a[2][2]{}; };
Mat mul(Mat A, Mat B) {
    Mat C;
    for (int i = 0; i < 2; i++)
        for (int k = 0; k < 2; k++)
            for (int j = 0; j < 2; j++)
                C.a[i][j] = (C.a[i][j] + A.a[i][k] * B.a[k][j]) % P;
    return C;
}
int main() {
    ll n; cin >> n;
    if (n <= 2) { cout << 1 << endl; return 0; }
    Mat A; A.a[0][0] = A.a[0][1] = A.a[1][0] = 1;
    Mat R; R.a[0][0] = R.a[1][1] = 1;  // 单位阵
    ll e = n - 1;
    while (e) { if (e & 1) R = mul(R, A); A = mul(A, A); e >>= 1; }
    cout << R.a[0][0] << endl;
    return 0;
}

🔍 代码逐段解析

Mat / mul —— 2×2 矩阵乘法,三重循环 i-k-j,每个累加立即 % P。

main 主体 —— n ≤ 2 特判输出 1;A 初始化 [[1,1],[1,0]];R 是单位阵;随后标准二进制快速幂对矩阵执行。

答案提取 —— 幂次 e = n − 1,结果取 R.a[0][0] 即 F(n)。

易错点 —— ① 单位阵未置 1;② e 写成 n;③ n ≤ 2 未特判。

考点:快速幂的思想从"数的乘法"迁移到"矩阵乘法";单位阵替代了数字 1 的角色。

易错:单位阵写成全 0;矩阵乘法循环顺序写错导致重复累加;n ≤ 2 时没特判。

C3数论分析

给定素数 p = 13 与底数 a = 3,问:a¹, a², a³, … 模 13 的取值会形成一个循环,循环节长度是多少?如果想知道"3 的多少次方模 13 等于 5",该怎么暴力求解?

查看参考答案

循环节长度:依次计算 3, 9, 27≡1, 所以序列是 3, 9, 1, 3, 9, 1, …,循环节长度 = 3。

为什么长度是 3:由费马小定理 3^(12) ≡ 1,所以循环节长度必然整除 p − 1 = 12。枚举 12 的因子(1, 2, 3, 4, 6, 12),最小使 3^k ≡ 1 的 k 就是 3。这个 k 称为 3 模 13 的阶。

求离散对数:p 较小时直接暴力枚举 k = 0, 1, 2, …,逐个算 3^k mod p 直到等于 5;p 较大时用 BSGS(大步小步)算法,复杂度约 O(√p)。本题 3¹ = 3、3² = 9、3³ = 27 ≡ 1、…、3⁵ = 243 ≡ 9、3⁶ ≡ 1、3⁷ ≡ 3、3⁸ ≡ 9、3⁹ ≡ 1 … 由于 5 不在循环里,说明无解(因为 3 生成的子群只有 {1, 3, 9})。

考点:模运算下的循环结构、费马小定理对循环节长度的约束、阶与离散对数的概念。

易错:以为循环节长度一定是 p − 1(实际只能是它的因子);暴力枚举时忘记存在"无解"的情形。

C4综合编程

给定素数 p 与 n(n < p ≤ 10⁷),求 n!⁻¹ mod p,并进一步回答 q 次询问:区间 [l, r] 内所有数的乘积的逆元。

查看参考答案

思路:预处理前缀积 pre[i] = 1 × 2 × … × i mod p。区间乘积 prod(l, r) = pre[r] × inv(pre[l−1])。而「乘积的逆元」= inv(pre[r]) × pre[l−1],只需把 pre 数组的每一项都求一遍逆元即可 O(1) 回答。

关键观察:对同一批数据,逆元可以"批量"求 —— 用线性递推表或对 pre 数组整体处理,避免每次询问都做一次快速幂。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXN = 10000005;
ll pre[MAXN], invPre[MAXN];
ll p;
ll qpow(ll b, ll e) {
    ll r = 1 % p; b %= p;
    while (e) { if (e & 1) r = r * b % p; b = b * b % p; e >>= 1; }
    return r;
}
int main() {
    ll n, q; scanf("%lld %lld %lld", &n, &p, &q);
    pre[0] = 1;
    for (int i = 1; i <= n; i++) pre[i] = pre[i - 1] * i % p;
    invPre[n] = qpow(pre[n], p - 2);          // 一次快速幂
    for (int i = n; i >= 1; i--) invPre[i - 1] = invPre[i] * i % p;  // 倒推全部
    while (q--) {
        int l, r; scanf("%d %d", &l, &r);
        ll prod = pre[r] * invPre[l - 1] % p;       // 区间乘积
        ll ans  = invPre[r] * pre[l - 1] % p;       // 区间乘积的逆元
        printf("%lld %lld\n", prod, ans);
    }
    return 0;
}

🔍 代码逐段解析

前缀积表 —— pre[0] = 1,pre[i] = pre[i-1] * i % p,pre[i] 即 i!。

批量逆元 —— invPre[n] = qpow(pre[n], p-2) 一次快速幂,再倒推 invPre[i-1] = invPre[i] * i % p,O(n) 拿到全部阶乘逆元。

查询 —— 区间乘积 pre[r] * invPre[l-1] % p;区间乘积的逆元 invPre[r] * pre[l-1] % p(即 l..r 各数逆元之积,等于乘积的逆元),两者都 O(1)。

易错点 —— ① 每次询问都快速幂求逆元退化到 O(q log p);② l = 1 时 invPre[l-1] = invPre[0],倒推必须推到下标 0。

验证:p = 13,n = 5。pre = [1, 1, 2, 6, 24≡11, 120≡3]。区间 [2,4] 乘积 = 2×3×4 = 24 ≡ 11,而 pre[4]×invPre[1] = 11×1 = 11 ✓;其逆元 11⁻¹ mod 13 = 6(11×6 = 66 ≡ 1)✓。

考点:前缀积 + 逆元的批量处理;区间查询 O(1)。

易错:每次询问都调用快速幂求逆元,复杂度退化到 O(q log p);把「乘积的逆元」误当成「逆元的乘积」(虽然两者相等,但前者不需要额外求逆)。

🏆 洛谷原题精选(12道)

以下 12 道题都围绕"同余式 / 快速幂 / 逆元 / 组合数取模"展开,按难度从普及- 到提高排列,建议按顺序刷。

1
P1226
【模板】快速幂||取余运算普及-
2
P1965
转圈游戏普及-
3
P1029
最大公约数和最小公倍数问题普及-
4
P3390
【模板】矩阵快速幂普及+
5
P3811
【模板】乘法逆元普及+
6
P2613
【模板】有理数取余普及+
7
P4549
【模板】裴蜀定理普及+
8
P2822
组合数问题提高
9
P1313
计算系数提高
10
P1082
同余方程提高
11
P3807
【模板】卢卡斯定理提高
12
P3197
[SDOI2008] 越狱提高