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

费马小定理 与 威尔逊定理

两条只用一行公式就能写下的定理,一条把 10¹⁸ 次幂压缩成几十次乘法,另一条把"阶乘"和"素数"牢牢绑在了一起。

6 大模块 2 条经典定理 重排证明直觉 O(log p) 降幂 12 道洛谷原题
本讲 · 第 44/52 讲费马小定理与威尔逊定理
模块 01

情景导航

从一个"指数大得离谱"的题目说起,看看两条三百年前的定理如何在考场上救你一命。

故事一句话省下十亿次乘法

模拟赛的压轴题来势汹汹:"给定整数 a(1 ≤ a < 10⁹)与一个大整数 k,求 a^k mod 1000000007,其中 k 是长度不超过 10⁵ 的十进制数。"

小 Q 一看 k 的长度,头皮发麻:这个指数连 long long 都装不下,快速幂的 while (exp) 循环根本没法用 —— 因为 exp 根本存不进去。

这时同桌的学长只说了一句话:"因为 1000000007 是素数,所以 a^(1000000006) ≡ 1 (mod 1000000007)。"

小 Q 愣住了。学长接着说:"指数每增加 1000000006,幂值就转了一圈回到 1。所以你只需要知道 k 除以 1000000006 的余数,其余部分全是 1 的连乘,都可以扔掉。"

于是那个上万位的 k,被"边读边取模"压缩成了一个 30 位左右的整数,再用快速幂一算,程序瞬间跑完。

a^k mod p = a^(k mod (p−1)) mod p   (p 为素数, gcd(a, p) = 1)

这条"降幂公式"背后,就是本讲的主角 —— 费马小定理。而它的"兄弟"威尔逊定理,则给出了素数的另一种刻画:(p−1)! ≡ −1 (mod p) 当且仅当 p 是素数。

💡 两条定理的地位

费马小定理是它所在知识体系里使用频率最高的结论:求逆元、降幂、判定阶、卢卡斯定理、RSA 加密,全都建立在它之上。它刻画的是"模素数乘法群的循环长度"。

威尔逊定理则是数学上的一颗明珠:它给出了素数的充要条件(不需要"已知 p 是素数"这个前提),是"用乘法性质刻画素数"的最优雅范例。但它的计算量太大,只能用于小范围验证或理论推导。

这两条定理都是下一讲欧拉定理的特例:把"p − 1"换成"φ(m)",把"p 是素数"换成"gcd(a, m) = 1",就得到了适用范围更广的欧拉定理。

①生活类比
🎡

摩天轮 = 幂的周期

摩天轮每转 12 秒回到原位。知道"经过时间 mod 12"就够判断位置,多转的整圈毫无影响。

→ a^(p−1) ≡ 1
🃏

洗牌 = 完全剩余系重排

把 1..6 这 6 张牌各乘 3 再模 7,得到的仍是这 6 张牌,只是顺序变了 —— 乘积不变。

→ 重排证明的核心
🔑

反向钥匙 = 逆元

费马小定理直接告诉你:a 的反向钥匙就是 a^(p−2),不用慢慢试。

→ inv(a) = a^(p−2)
🧪

石蕊试纸 = 威尔逊判定

把 (p−1)! 滴进模 p 的试纸:显"−1"就是素数,显别的数就是合数 —— 一次试验给出确定结论。

→ 充要条件
②核心定义

费马小定理(表述一):设 p 为素数,整数 a 满足 gcd(a, p) = 1,则 a^(p−1) ≡ 1 (mod p)。

费马小定理(表述二):设 p 为素数,则对任意整数 a 都有 a^p ≡ a (mod p)。这一版不需要互质条件,甚至对 a ≡ 0 也成立。

降幂公式:p 为素数且 gcd(a, p) = 1 时,a^k mod p 只与 k mod (p−1) 有关。

威尔逊定理:(p−1)! ≡ −1 (mod p) 当且仅当 p 是素数。这是素数的充要刻画。

完全剩余系:模 m 下取遍所有余数类的 m 个整数构成的集合。模素数 p 的非零完全剩余系就是 {1, 2, …, p−1}。

📌 本讲核心知识地图

板块核心内容对应大纲
费马小定理a^(p−1) ≡ 1 与 a^p ≡ a,重排证明【7】费马小定理
求逆元inv(a) ≡ a^(p−2) (mod p)【7】费马小定理
降幂a^k ≡ a^(k mod (p−1)) (mod p)【7】费马小定理
与欧拉定理p 为素数时 φ(p) = p−1,欧拉定理退化为费马小定理【7】欧拉定理与欧拉函数
威尔逊定理(p−1)! ≡ −1 (mod p) ⟺ p 为素数【7】威尔逊定理
推论(p−2)! ≡ 1 (mod p)【7】威尔逊定理
③核心口诀
🔑 费马小定理

模是素数就转轮,指数减一再回一;
求逆就用 p 减二,降幂就模 p 减一。

🔑 威尔逊定理

阶乘到 p 减一,结果必是负一;
反过来也成立,素数判定是充要。

🔑 使用前提

费马要素数 + 互质,两个条件缺一不可;
威尔逊只能小范围验,阶乘一爆全是泪。

模块 02

知识探究

先看懂费马小定理为什么成立,再把它变成求逆元与降幂两把刀,最后理解威尔逊定理的充要性与它的局限。

1费马小定理与"重排"证明 核心

定理的两句话先摆在这里,注意条件差别:

形式一:a^(p−1) ≡ 1 (mod p)
  • 条件:p 为素数且 gcd(a, p) = 1
  • 适用:a 不被 p 整除
  • 用途:求逆元、降幂、判定阶
  • 等价写法:a · a^(p−2) ≡ 1
形式二:a^p ≡ a (mod p)
  • 条件:p 为素数(不需要互质)
  • 适用:包括 a 是 p 的倍数的情形
  • 用法:两边同乘 a 即可由形式一推出
  • 反过来,若 a mod p ≠ 0 则可约去 a 得到形式一

📐 重排证明:为什么指数"转一圈"必回 1?

第 1 步:观察乘法。 考虑 p − 1 个数:1, 2, 3, …, p−1。它们恰好代表了模 p 下所有非零的余数类。

第 2 步:全体乘以 a。 把这 p − 1 个数都乘以 a,得到 a, 2a, 3a, …, (p−1)a。

第 3 步:证明互不相同。若 a·i ≡ a·j (mod p),则 a(i − j) ≡ 0 (mod p)。由于 gcd(a, p) = 1,可以约去 a,得 i ≡ j (mod p)。而 i, j 都在 1 到 p−1 之间,所以只能 i = j。

第 4 步:得出结论 —— 这是一次重排。p − 1 个互不相同、且都非零(因为 a 与 i 都不被 p 整除,乘积也不被 p 整除)的余数类,正好就是原来那 p − 1 个,只是顺序被打乱了。

第 5 步:两个乘积相等。既然只是重排,那么全体相乘的结果相同:

(a·1)(a·2)⋯(a·(p−1)) ≡ 1·2⋯(p−1) (mod p)

⟹ a^(p−1) · (p−1)! ≡ (p−1)! (mod p)

第 6 步:约去 (p−1)!。由于 p 是素数,它不整除 1, 2, …, p−1 中的任何一个,所以 gcd((p−1)!, p) = 1,(p−1)! 有逆元,可以约掉,最终得到 a^(p−1) ≡ 1 (mod p)。∎

注意最后一步为什么必须"p 是素数":如果 p 是合数(例如 p = 4),(p−1)! = 3! = 6 与 4 的 gcd 是 2,没有逆元,约不掉,定理也就不成立。

下面用 p = 7、a = 3 观察"重排"这件事:把 1 到 6 每个数乘 3 再取模 7,得到的仍然是 1 到 6 的一个排列。

1→3
2→6
3→2
4→5
5→1
6→4

结果集合 {3, 6, 2, 5, 1, 4} 正是 {1,2,3,4,5,6},一个不多一个不少

原数 i3i3i mod 73i 是否落在 1..6 内
133✔
266✔
392✔
4125✔
5151✔
6184✔
📊 乘积验证
左边3×6×2×5×1×4
=720
右边1×2×3×4×5×6
=720
两边都是 720,且 3^6 = 729 ≡ 1 (mod 7) ✓ 这就是 3^(7−1) ≡ 1
🎬 交互演示:重排证明逐步走(p = 7, a = 3)
1起点:非零余数类 1, 2, 3, 4, 5, 6,它们的乘积是 6! = 720。
2全体乘 3 再模 7:3, 6, 2, 5, 1, 4 —— 逐个检查:3×1=3、3×2=6、3×3=9≡2、3×4=12≡5、3×5=15≡1、3×6=18≡4。
3互不相同:若 3i ≡ 3j (mod 7),因 gcd(3,7)=1 可约去 3,得 i ≡ j,矛盾 —— 所以结果是一个"重排"。
4重排不改变乘积:3×6×2×5×1×4 = 720 = 6!。
5左边提出公因子:3^6 × 6! ≡ 6! (mod 7)。
6约去 6!(gcd(720,7)=1 有逆元):3^6 ≡ 1 (mod 7) —— 费马小定理得证。验算 729 = 7×104 + 1 ✓
💡 整个证明只用了两件事:乘法保持同余 + 非零元素可约去,p 为素数保证了这两点。
2两大应用:求逆元与降幂 考场主力
1
🔑
求逆元

inv(a) ≡ a^(p−2) (mod p)

2
🪜
降幂

a^k ≡ a^(k mod (p−1))

🔑 应用一:求乘法逆元

由 a^(p−1) ≡ 1,两边同乘 a 的逆元(或直接移项)得 a · a^(p−2) ≡ 1。对照逆元定义 a · x ≡ 1,立刻得到 inv(a) = a^(p−2) mod p。

整个求逆元过程只有一次快速幂,复杂度 O(log p),代码长度不超过 10 行,是竞赛中求单个逆元最常用的手段。

前提检查清单:① p 必须是素数;② a 不被 p 整除。两条都满足才成立。

🪜 应用二:降幂(超大指数压缩)

由 a^(p−1) ≡ 1 可以推出:a^n 的值只取决于 n mod (p−1)。

推导:设 n = q(p−1) + r,则

a^n = (a^(p−1))^q × a^r ≡ 1^q × a^r = a^r (mod p)

所以不管指数有 10 位还是 10⁵ 位,只要把它对 p − 1 取模,压成小整数就能算。

工程技巧:指数是字符串时,用 r = (r * 10 + c − '0') % (p−1) 边读边取模,一趟 O(长度) 扫描搞定。

重要澄清:只要 gcd(a, p) = 1,把指数对 p − 1 取模就是严格的。很多同学习惯写成"对 p 取模"—— 那是把幂值取模和指数取模搞混了,必然出错。

🎬 交互演示:降幂求解 2^1000000 mod 13 的四个步骤
1确认前提:模数 13 是素数,底数 2 与 13 互质(gcd(2,13) = 1),满足费马小定理的全部条件,可以降幂。
2写出周期:由费马小定理 2^12 ≡ 1 (mod 13),说明幂值每 12 个指数"转一圈"回到 1。所以指数只需要知道除以 12 的余数。
3压缩指数:计算 1000000 mod 12。1000000 = 12 × 83333 + 4,所以余数 r = 4。原题被化简为 2^4 mod 13。
4快速幂求值:2^4 = 16,16 − 13 = 3,所以答案 2^1000000 mod 13 = 3。原本需要一百万次乘法,现在只用了 3 轮。
💡 核心直觉:模素数 p 的幂运算是一个"周期为 p−1 的摩天轮",指数多转的整圈对结果毫无影响,只看余数就够了。
✅ 正确的降幂写法

指数对 p − 1 取模:r = k % (p - 1),然后 qpow(a, r, p)。

指数是字符串时:r = (r * 10 + c - '0') % (p - 1)。

适用前提:p 素数且 gcd(a, p) = 1。

❌ 常见的错误写法

指数对 p 取模:r = k % p —— 周期长度弄错了。

模数不是素数却照用降幂 —— 结论失效。

底数是 p 的倍数(如 a = p, a = 2p)时降幂 —— 此时 a^k ≡ 0,任何降幂都无意义。

3与欧拉定理的关系:一个特例 承上启下

下一讲要学的欧拉定理是:若 gcd(a, m) = 1,则 a^φ(m) ≡ 1 (mod m),其中 φ(m) 是 1..m 中与 m 互质的数的个数(欧拉函数)。

🔗 费马小定理就是 m 取素数时的欧拉定理

当 m = p 为素数时,1 到 p 之间的数除了 p 本身全都与 p 互质,所以 φ(p) = p − 1。

把 φ(p) = p − 1 代入欧拉定理 a^φ(p) ≡ 1,立刻得到 a^(p−1) ≡ 1 (mod p) —— 正是费马小定理。

反过来不成立:欧拉定理对任意 m 都适用,费马小定理只是一个特例。因此做题时如果模数是合数,就必须用欧拉定理(或扩展欧拉定理),不能退化成费马。

定理条件结论适用范围
费马小定理p 为素数,gcd(a, p) = 1a^(p−1) ≡ 1 (mod p)模数必须是素数
欧拉定理gcd(a, m) = 1(m 任意)a^φ(m) ≡ 1 (mod m)任意模数,只要互质
扩展欧拉定理m 任意,a 任意b ≥ φ(m) 时 a^b ≡ a^(b mod φ(m) + φ(m))最通用,不要求互质
🔑 关系口诀

费马是欧拉的素数特例:φ(p) = p − 1;
模数一旦不是素数,就老老实实换成 φ(m) 或扩展形式。

4威尔逊定理:素数的充要刻画 高频

威尔逊定理:对整数 p ≥ 2,(p−1)! ≡ −1 (mod p) 当且仅当 p 是素数。

📐 为什么合数一定不满足?

情形一:p 是合数且能分解成两个不同的因子(p = a·b,1 < a < b < p)。此时 a 与 b 都出现在 1, 2, …, p−1 当中,于是 a·b = p 整除 (p−1)!,所以 (p−1)! ≡ 0 (mod p),绝不等于 −1。

情形二:p = q²(平方数)。若 q > 2,则 q 与 2q 都小于 q² = p,所以 q 与 2q 都在阶乘里,乘积 2q² 是 p 的倍数,于是 (p−1)! ≡ 0。特例 p = 4 = 2²:3! = 6 ≡ 2 (mod 4),而 −1 ≡ 3,不等。

素数情形为什么是 −1:把 2, 3, …, p−2 这些数按"逆元成对"配对。对每个 a ∈ [2, p−2],它的逆元 a⁻¹ 也在同一区间内且不等于 a(若 a = a⁻¹ 则 a² ≡ 1,解得 a = 1 或 a = p−1,都不在区间内)。因此这 p−3 个数两两配对,每对的乘积都是 1,整体乘积为 1。于是

(p−1)! = 1 × (配对乘积 1) × (p−1) ≡ 1 × (−1) = −1 (mod p)

1
🧩
配对

2..p−2 中的数按逆元两两配对

2
✖️
乘积为 1

每对 a·a⁻¹ ≡ 1

3
✅
剩下 1 与 p−1

1 × (p−1) ≡ −1

🎬 交互演示:威尔逊配对逐步走(p = 11)
1目标:证明 10! ≡ −1 ≡ 10 (mod 11)。把 2 到 9 这 8 个"中间元素"按逆元配对。
2第一对:2 的逆元是 6(2×6 = 12 ≡ 1),配成一对,乘积 ≡ 1。
3第二对:3 的逆元是 4(3×4 = 12 ≡ 1),乘积 ≡ 1。
4第三对:5 的逆元是 9(5×9 = 45 ≡ 1),乘积 ≡ 1。
5第四对:7 的逆元是 8(7×8 = 56 ≡ 1),乘积 ≡ 1。
68 个中间元素全部配完,每对乘积都是 1。剩下没配对的只有 1 和 10(自逆元 ±1 不能出现在中间区间)。
7结论:10! = 1 × (四对乘积) × 10 ≡ 1 × 10 ≡ −1 (mod 11) ✓ —— 威尔逊定理在 p = 11 上完整呈现。
💡 为什么中间元素不会"自己配自己":a² ≡ 1 的解只有 a ≡ ±1(即 1 和 10),它们都不在 2..9 里。

📌 常用推论:(p−2)! ≡ 1 (mod p)

把 (p−1)! 拆开:(p−1)! = (p−1) × (p−2)! ≡ (−1) × (p−2)!。

又 (p−1)! ≡ −1,所以 (−1) × (p−2)! ≡ −1,两边同乘 −1 得 (p−2)! ≡ 1 (mod p)。

这个推论在计算"含 (p−2)! 的式子"时非常好用,等价于直接说 "inv((p−2)!) = 1"。

✅ 威尔逊定理的优势

是充要条件 —— 不需要预先知道 p 是不是素数,算完阶乘就能判定。

纯乘法刻画 —— 只用"阶乘 + 同余",非常适合理论推导与证明题。

可推导出 (p−2)! ≡ 1,帮助化简含阶乘的复杂表达式。

❌ 威尔逊定理的局限

计算量爆炸:判断 p = 10⁹ 是否为素数需要算 (10⁹ − 1)!,大约 10⁹ 次乘法并需要高精度中间量,完全不现实。

实际判定素数用 Miller-Rabin(概率)或 试除 / 埃氏筛 / 线性筛(小范围)。

所以威尔逊定理在竞赛中主要用于 小范围验证(p ≤ 20 左右) 与 理论证明。

5选择题中的经典考法 初赛
考法典型问法应对要点
条件辨析"下列说法正确的是"(涉及是否要求素数、是否要求互质)费马形式一要求素数 + 互质;形式二只要求素数
数值计算求 3^(1000) mod 7、求 5^(2024) mod 11先算指数 mod (p−1),再快速幂
求逆元求 3 在模 11 下的逆元a^(p−2);也可用小表凑
充要性判断"(p−1)! ≡ −1 (mod p) 是 p 为素数的什么条件"威尔逊定理给出的是充要条件
推论计算求 (p−2)! mod p答案为 1(p 为素数时)
适用范围"下列哪些场合不能用费马小定理"模数合数、底数与模数不互质,都不行
初赛速判技巧:看到"小素数 p 与大指数"→ 立刻降幂;看到"模数是不是素数"→ 立刻确认定理可用性;看到 (p−1)! 或 (p−2)! → 立刻联想威尔逊定理。
案例案例1:手算 3¹⁰⁰⁰ mod 7 入门

题目:求 3^1000 mod 7 的值,要求给出不依赖计算机的手算过程,并用代码验证。

🔍 手算过程

第1步:确认条件 — 7 是素数,gcd(3, 7) = 1,满足费马小定理。

第2步:写出周期 — 3^6 ≡ 1 (mod 7),周期长度为 6。

第3步:压缩指数 — 1000 ÷ 6 = 166 余 4,即 1000 = 6 × 166 + 4。

第4步:降幂 — 3^1000 = (3^6)^166 × 3^4 ≡ 1^166 × 3^4 = 81。

第5步:取模 — 81 mod 7 = 81 − 77 = 4。

结论:3^1000 mod 7 = 4。整个手算只用到一次除法和一次小乘法。

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

// 快速幂:base^exp 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;
}

int main() {
    ll a = 3, p = 7;
    string k = "1000";

    // 指数对 p-1 取模(字符串边读边取模,支持任意长度)
    ll r = 0;
    for (char c : k) r = (r * 10 + (c - '0')) % (p - 1);

    cout << "指数降幂后 r = " << r << endl;
    cout << "答案 = " << qpow(a, r, p) << endl;

    // 对照:直接用快速幂算原始指数
    cout << "直接计算 3^1000 mod 7 = " << qpow(a, 1000, p) << endl;
    return 0;
}
运行结果
指数降幂后 r = 4
答案 = 4
直接计算 3^1000 mod 7 = 4

两种方法结果一致 ✓
手算过程:3^6 ≡ 1,1000 = 6×166 + 4,3^1000 ≡ 3^4 = 81 ≡ 4 (mod 7)

💡 一个小验证:3 模 7 的幂序列

3¹ = 3、3² = 9 ≡ 2、3³ ≡ 6、3⁴ ≡ 4、3⁵ ≡ 5、3⁶ ≡ 1、3⁷ ≡ 3 …

可以看到序列 {3, 2, 6, 4, 5, 1} 确实是长度 6 的循环,而 6 恰好整除 p − 1 = 6。一般地,循环长度一定整除 p − 1,这是费马小定理的直接推论。

案例案例2:超大指数降幂(10⁵ 位指数) 实战

题目:给定素数 p = 1000000007、整数 a(1 ≤ a < p),以及一个长度不超过 10⁵ 的十进制整数 b,求 a^b mod p。

🔍 分析过程

第1步:判断 b 能否直接读入 — 10⁵ 位远超 long long,必须用字符串。

第2步:确认降幂合法性 — p 是素数,且题目保证 1 ≤ a < p 所以 gcd(a, p) = 1,费马小定理可用。

第3步:边读边取模 — 一边扫描字符串一边做 r = (r * 10 + digit) % (p − 1),把巨大指数压缩到 [0, p−2]。

第4步:快速幂 — 用压缩后的 r 计算 a^r mod p,O(log p)。

第5步:注意 r = 0 的情况 — 若 b 是 p−1 的倍数,r = 0,此时答案应为 1(因为 a^(p−1) ≡ 1)。快速幂中 res 初值 1 % p 恰好能自动给出正确结果。

复杂度:O(len + log p),len ≤ 10⁵,毫无压力。

C++超大指数降幂 — 完整代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

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;
}

int main() {
    ll a, p;
    string b;
    cin >> a >> p >> b;          // b 是字符串形式的巨大指数

    if (a % p == 0) {         // 底数是 p 的倍数,降幂不适用
        cout << (b == "0" ? 1 % p : 0) << endl;
        return 0;
    }

    ll r = 0;
    for (char c : b)
        r = (r * 10 + (c - '0')) % (p - 1);   // 指数模 p-1

    cout << qpow(a, r, p) << endl;
    return 0;
}
🔍 代码逐段解析 · 超大指数降幂
1qpow —— 与案例1完全相同的快速幂,复用即可。
2cin >> a >> p >> b —— b 是 string,10⁵ 位指数只有字符串能接住。
3if (a % p == 0) —— 底数是 p 的倍数时降幂公式失效:b > 0 答案直接是 0,b = "0" 时答案 1 % p。特判后立即返回。
4for 循环降幂 —— r = (r*10 + (c-'0')) % (p-1),一趟扫描把 10⁵ 位指数压进 [0, p−2],O(len)。
5cout << qpow(a, r, p) —— 用压缩后的小指数快速幂,O(log p)。
6r = 0 的正确性 —— 若 b 是 p−1 的倍数则 r = 0,qpow 返回 1 % p = 1,与 a^(p−1) ≡ 1 完全一致,无需额外处理。
⚠️ 易错点:① 漏掉 a % p == 0 特判;② 指数模写成 p;③ 试图用 stoll 读指数。
运行结果
输入: 2 1000000007 1000
输出: 688423210
(与直接用 int 型指数算 2^1000 结果一致)

输入: 3 1000000007 1000000006
输出: 1
(指数正好等于 p-1,3^(p-1) ≡ 1 ✓ 这就是费马小定理本身)

输入: 5 1000000007 1000000006000000000000
输出: 1
(指数是 p-1 的倍数,答案同样是 1)

输入: 7 1000000007 1234567890123456789012345678901234567890
输出: 827999512
(40 位指数,long long 装不下,字符串边读边取模轻松解决;已用 g++ 实测验证)

💡 为什么必须处理 a % p == 0?

当 a 是 p 的倍数时,a^b ≡ 0(b > 0),根本谈不上"周期"。而降幂公式的推导依赖 a^(p−1) ≡ 1,此时不成立。若不加判断,程序会算出错误结果。

另外 1 % p 的写法在这里再次派上用场:答案初始值设为 1 % p 后,当 r = 0 时快速幂直接返回 1,与 a^(p−1) ≡ 1 完全吻合。

案例案例3:用威尔逊定理验证素性 概念

题目:对 2 到 40 之间的每个整数 n,计算 (n−1)! mod n,观察"结果等于 n−1"与"n 是素数"这两件事是否完全同步。

🔍 分析过程

第1步:定理的形式转换 — (p−1)! ≡ −1 (mod p) 中的 −1 在余数范围内写作 p − 1,所以判定条件就是 (n−1)! mod n == n − 1。

第2步:暴力计算阶乘 — 对每个 n 从 2 累乘到 n−1,过程取模防止溢出。

第3步:对照朴素判定 — 同时用试除法判断 n 是不是素数,两列结果并排比较。

第4步:观察结论 — 两列完全一致,直观验证了威尔逊定理的充要性。

复杂度:单次 O(n),n ≤ 40 时可以忽略;但换成 n = 10⁹ 就完全不可行了 —— 这正是理论好用、工程不好用的典型例子。

C++威尔逊定理验证小素数 — 完整代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

// 试除法判断素数
bool isPrime(ll n) {
    if (n < 2) return false;
    for (ll i = 2; i * i <= n; i++)
        if (n % i == 0) return false;
    return true;
}

// 按威尔逊定理判定:(n-1)! mod n 是否等于 n-1
bool wilsonTest(ll n) {
    if (n < 2) return false;
    ll fact = 1 % n;
    for (ll i = 1; i <= n - 1; i++) fact = fact * i % n;
    return fact == n - 1;
}

int main() {
    printf("%4s %14s %14s %10s\n", "n", "(n-1)! mod n", "试除法", "威尔逊");
    for (ll n = 2; n <= 20; n++) {
        ll fact = 1 % n;
        for (ll i = 1; i <= n - 1; i++) fact = fact * i % n;
        printf("%4lld %14lld %14s %10s\n", n, fact,
               isPrime(n) ? "素数" : "合数",
               wilsonTest(n) ? "素数" : "合数");
    }
    return 0;
}
运行结果
   n  (n-1)! mod n           试除法       威尔逊
   2             1            素数        素数
   3             2            素数        素数
   4             2            合数        合数
   5             4            素数        素数
   6             0            合数        合数
   7             6            素数        素数
   8             0            合数        合数
   9             0            合数        合数
  10             0            合数        合数
  11            10            素数        素数
  12             0            合数        合数
  13            12            素数        素数
  14             0            合数        合数
  15             0            合数        合数
  16             0            合数        合数
  17            16            素数        素数
  18             0            合数        合数
  19            18            素数        素数
  20             0            合数        合数

观察:只要 n 是素数,(n-1)! mod n 恰好等于 n-1;
      只要 n 是合数(n ≥ 4),结果一定不等于 n-1。两列完全同步 ✓

💡 两个细节值得注意

细节一:n = 1 时定理不适用(威尔逊定理要求 p ≥ 2),所以代码里从 n = 2 开始,且用 1 % n 处理 n = 1 的退化情形。

细节二:合数的结果大多是 0 而不是别的值 —— 因为一旦 n 有真因子 a 与 b(a·b = n),阶乘里就包含了这两个因子,乘积直接被 n 整除。只有 n = 4 是个例外(结果是 2),因为 4 = 2² 而阶乘里只有一个 2。这个例外正是证明中需要单独讨论 q² 的原因。

模块 03

实践应用

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

题1费马小定理求逆元(多组询问)
题1模板题

逆元查询

题目背景:模素数意义下求逆元是数论题的常量操作,必须做到"看到就写得出"。

输入格式:第一行两个整数 p(素数)与 q;接下来 q 行每行一个整数 a(1 ≤ a < p)。

输出格式:共 q 行,每行一个整数表示 a 在模 p 下的逆元。

输入样例:11 3
3
5
10

输出样例:4
9
10

数据范围:2 ≤ p ≤ 10⁹,1 ≤ q ≤ 10⁵。时间限制 1s,空间限制 256MB。

方法一:枚举试探 太慢

从 1 到 p − 1 逐个尝试 a·x ≡ 1,单次最坏 O(p),p = 10⁹ 时完全不可用。

方法二:费马小定理 推荐

直接输出 a^(p−2) mod p,单次 O(log p) ≈ 30 轮乘法,q = 10⁵ 时总共约 3×10⁶ 次运算,轻松通过。

前提:p 必须是素数(题目已保证),且 a 不被 p 整除(题目保证 1 ≤ a < p)。

验证:3 × 4 = 12 ≡ 1 (mod 11) ✓;5 × 9 = 45 ≡ 1 (mod 11) ✓;10 × 10 = 100 ≡ 1 (mod 11) ✓(10 是自己的逆元,因为 10 ≡ −1)。

C++费马小定理求逆元 — 完整代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

ll p;

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

// 费马小定理:inv(a) = a^(p-2) mod p(p 为素数)
ll inv(ll a) { return qpow(a, p - 2); }

int main() {
    int q;
    scanf("%lld %d", &p, &q);
    while (q--) {
        ll a; scanf("%lld", &a);
        printf("%lld\n", inv(a));
    }
    return 0;
}

🔍 代码逐段解析 · 费马小定理求逆元(多组询问)

全局 ll p —— qpow 与 inv 都要用模数,放全局共享。

qpow —— 标准快速幂,res = 1 % p 兼容 p 的边界。

inv(a) —— 一行核心:qpow(a, p - 2),指数 p−2 来自 a·a^(p−2) ≡ 1。

main —— 读 p、q 后循环 q 次,每次读 a 直接输出 inv(a)。q = 10⁵ 次 × 30 轮乘法,轻松通过。

易错点 —— ① 指数写成 p−1;② 若题目不保证 a < p,需先 a %= p(本题 1 ≤ a < p 已保证)。

运行结果
输入: 11 3
      3
      5
      10
输出: 4
      9
      10

验算:3 × 4  = 12  ≡ 1 (mod 11) ✓
      5 × 9  = 45  ≡ 1 (mod 11) ✓
      10 × 10 = 100 ≡ 1 (mod 11) ✓

💡 一个有趣的观察

当 a ≡ −1 (mod p) 时,a 的逆元就是它自己 —— 因为 (−1)² = 1。模 11 下 a = 10 正是这种情况。类似的还有 a = 1(逆元是 1),以及 p = 7 时 a = 6。

一般地,方程 a² ≡ 1 (mod p)(p 为素数)只有两个解 a ≡ ±1,这两个元素各自构成"自逆元对"。

题2威尔逊定理做素性判断
题2定理应用

用阶乘判定素数

题目背景:威尔逊定理给出了素数的充要条件,本题用它来判断一个小范围内的整数是否为素数。

输入格式:一行一个整数 n。

输出格式:若 n 为素数输出 Prime,否则输出 Composite,并额外输出 (n−1)! mod n 的值。

输入样例:7

输出样例:(n-1)! mod n = 6
威尔逊判定:Prime
试除判定:Prime
两种方法一致 ✓

数据范围:2 ≤ n ≤ 10⁶。时间限制 1s,空间限制 256MB。

方法一:威尔逊定理 充要

从 1 累乘到 n − 1 并全程取模,最后判断结果是否等于 n − 1。复杂度 O(n),n = 10⁶ 时约 10⁶ 次乘法,可以接受。

方法二:试除法 工程首选

只需试除到 √n,复杂度 O(√n) ≈ 10³,快上千倍。这是实际工程中判断单个数的素性的标准做法。

方法对比结论:威尔逊定理在数学上很美(充要条件、纯乘法刻画),但计算代价过高,只适合小 n 或理论推导;真实场景一律用试除、埃氏筛、线性筛或 Miller-Rabin。本题两种方法结果必须一致。

C++威尔逊定理素性判断 — 完整代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

// 威尔逊定理:p 为素数 ⟺ (p-1)! ≡ -1 ≡ p-1 (mod p)
bool wilson(ll n) {
    if (n < 2) return false;
    ll fact = 1 % n;
    for (ll i = 1; i <= n - 1; i++) fact = fact * i % n;
    return fact == n - 1;
}

// 试除法:工程上真正使用的做法
bool trialDiv(ll n) {
    if (n < 2) return false;
    for (ll i = 2; i * i <= n; i++)
        if (n % i == 0) return false;
    return true;
}

int main() {
    ll n; cin >> n;
    ll fact = 1 % n;
    for (ll i = 1; i <= n - 1; i++) fact = fact * i % n;
    printf("(n-1)! mod n = %lld\n", fact);

    bool a = wilson(n), b = trialDiv(n);
    printf("威尔逊判定:%s\n", a ? "Prime" : "Composite");
    printf("试除判定  :%s\n", b ? "Prime" : "Composite");
    printf("两种方法%s\n", a == b ? "一致 ✓" : "不一致 ✗");
    return 0;
}

🔍 代码逐段解析 · 威尔逊素性判断

wilson(n) —— n < 2 返回 false;fact = 1 % n 起步累乘到 n−1 并逐步取模;返回 fact == n - 1(−1 的余数表示)。

trialDiv(n) —— 工程标准试除法,只试到 √n。

main —— 先独立算一遍阶乘输出 (n-1)! mod n 的值(题目要求),再分别调用两种判定并比较结果是否一致。

复杂度对照 —— 威尔逊 O(n) vs 试除 O(√n):n = 10⁶ 时差一千倍,这就是"定理之美"与"算法之效"的距离。

易错点 —— ① 判定写成 fact == −1;② 阶乘不取模溢出;③ wilson 与 main 里重复算了阶乘(此处为打印值,逻辑无碍但可优化)。

运行结果
输入: 7
输出: (n-1)! mod n = 6
      威尔逊判定:Prime
      试除判定  :Prime
      两种方法一致 ✓

输入: 12
输出: (n-1)! mod n = 0
      威尔逊判定:Composite
      试除判定  :Composite
      两种方法一致 ✓

输入: 4
输出: (n-1)! mod n = 2
      威尔逊判定:Composite
      试除判定  :Composite
      两种方法一致 ✓
(4 是唯一的"合数但阶乘结果非 0"的例外,结果 2 仍不等于 n-1=3)

💡 从威尔逊到 Miller-Rabin

既然威尔逊定理是充要条件,为什么不用它判素数?因为阶乘的代价是 O(n),对 64 位整数完全不可行。

现代方案是 Miller-Rabin:它基于费马小定理(若 n 是素数则 a^(n−1) ≡ 1)加上"平方根只有 ±1"的二次探测,用若干个底数做概率判定。虽然只是"极大概率正确",但复杂度只有 O(k log³n),工程上被广泛采用。

题3卢卡斯定理入门(大组合数取小素数模)
题3综合应用

组合数取模(模数为小素数)

题目背景:上一讲讲过用阶乘预处理求 C(n, m) mod p,但那要求 n 在 10⁶ 量级。若 n 达到 10¹⁸ 而 p 很小,就需要卢卡斯定理。

输入格式:第一行三个整数 n, m, p(p 为素数)。

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

输入样例:10 3 7

输出样例:1

数据范围:0 ≤ m ≤ n ≤ 10¹⁸,1 ≤ p ≤ 10⁵ 且 p 为素数。时间限制 1s,空间限制 256MB。

为什么不能直接用阶乘预处理 内存爆

n = 10¹⁸ 时连数组都开不出来,更不用说打表。

卢卡斯定理 正解

把 n 与 m 写成 p 进制,设 n = n₁p + n₀、m = m₁p + m₀(n₀, m₀ 是最低位),则

C(n, m) ≡ C(n₀, m₀) × C(n₁, m₁) × … (mod p)

递归写法:Lucas(n, m) = C(n mod p, m mod p) × Lucas(n / p, m / p) mod p,其中 C(a, b) 因 a, b < p 可以直接用阶乘与费马逆元计算。

终止条件:m = 0 时返回 1;m > n 时返回 0。

复杂度:递归深度 O(log_p n) ≤ 60,每次递归内部 O(p) 预处理或 O(log p) 单点计算,总复杂度可接受。

C++卢卡斯定理 — 完整代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

ll p;

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

// 计算 C(a, b) mod p,要求 a, b < p 且 p 为素数
ll comb(ll a, ll b) {
    if (b > a) return 0;
    ll num = 1, den = 1;
    for (ll i = 1; i <= b; i++) {
        num = num * ((a - i + 1) % p) % p;   // 分子:a(a-1)...(a-b+1)
        den = den * i % p;                        // 分母:b!
    }
    return num * qpow(den, p - 2) % p;       // 用费马小定理求逆元
}

// 卢卡斯定理:递归拆分成 p 进制的每一位
ll lucas(ll n, ll m) {
    if (m == 0) return 1 % p;
    return comb(n % p, m % p) * lucas(n / p, m / p) % p;
}

int main() {
    ll n, m; cin >> n >> m >> p;
    cout << lucas(n, m) << endl;
    return 0;
}
🔍 代码逐段解析 · 卢卡斯定理
1全局 ll p —— qpow、comb、lucas 三层都要用模数,放全局最简洁。
2qpow(base, exp) —— 快速幂,只在 comb 里用来求一次费马逆元。
3comb(a, b) —— 小组合数:b > a 直接返回 0;否则分子累乘 (a−i+1)、分母累乘 i,各步取模,最后 num * qpow(den, p-2) % p 用费马小定理除掉 b!。前提 a, b < p,由递归拆位保证。
4lucas(n, m) —— 递归核心:comb(n % p, m % p) * lucas(n / p, m / p) % p,把 n、m 按 p 进制逐位拆开相乘。
5终止条件 if (m == 0) —— 返回 1 % p(空集方案数为 1)。没有它递归永不停。
6main —— 读 n, m, p 直接输出 lucas(n, m)。n = 10¹⁸ 时递归深度仅 log_p n ≈ 4 层。
⚠️ 易错点:① 忘写 m == 0 终止;② comb 里分子分母漏取模;③ 试图对 10¹⁸ 直接打阶乘表。
运行结果
输入: 10 3 7
输出: 1
(验算:C(10,3) = 120,120 mod 7 = 1 ✓
  卢卡斯过程:n=10=(1,3)₇, m=3=(0,3)₇
  C(3,3) × C(1,0) = 1 × 1 = 1 ✓)

输入: 10 3 5
输出: 0
(验算:120 mod 5 = 0 ✓
  卢卡斯过程:n=10=(2,0)₅, m=3=(0,3)₅
  C(0,3) = 0 → 整体为 0 ✓)

输入: 1000000000000000000 500000000000000000 99991
输出: 69930
(n 达 10^18,普通递推完全不可行;卢卡斯只需约 4 层递归)

💡 卢卡斯定理背后的直觉

卢卡斯定理的证明来自多项式 (1 + x)^n 在模 p 下的性质:由费马小定理可推出 (1 + x)^p ≡ 1 + x^p (mod p),于是

(1 + x)^n = ((1 + x)^p)^(n/p) × (1 + x)^(n mod p) ≡ (1 + x^p)^(n/p) × (1 + x)^(n mod p)。

两边比较 x^m 的系数,就把原问题拆成了"高位部分"与"低位部分"两个独立子问题 —— 这正是 Lucas 递归式的来源。

模块 04

总结提升

①核心要点与万能模板
知识板块必须记住的核心前提条件
费马小定理(形式一)a^(p−1) ≡ 1 (mod p)p 素数 且 gcd(a,p)=1
费马小定理(形式二)a^p ≡ a (mod p)仅要求 p 素数
求逆元inv(a) ≡ a^(p−2) (mod p)同上
降幂a^k ≡ a^(k mod (p−1)) (mod p)同上
证明直觉乘 a 后完全剩余系被重排,乘积不变—
威尔逊定理(p−1)! ≡ −1 (mod p) ⟺ p 为素数p ≥ 2
威尔逊推论(p−2)! ≡ 1 (mod p)p 为素数
威尔逊的局限阶乘 O(p),只能小范围用—
与欧拉定理φ(p) = p − 1,费马是欧拉的特例—
卢卡斯定理C(n,m) ≡ ∏ C(nᵢ, mᵢ) (mod p)p 为素数,n 可极大
C++万能模板:费马逆元 + 降幂 + 威尔逊判定
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

/* ===== 快速幂 ===== */
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;
}

/* ===== 费马小定理求逆元(p 必须是素数,且 gcd(a,p)=1) ===== */
ll invFermat(ll a, ll p) { return qpow(a, p - 2, p); }

/* ===== 降幂:指数为字符串,对 p-1 取模 ===== */
ll reduceExp(const string &k, ll p) {
    ll r = 0;
    for (char c : k) r = (r * 10 + (c - '0')) % (p - 1);
    return r;
}

/* ===== 威尔逊定理判定(仅适合小 n,O(n)) ===== */
bool wilsonPrime(ll n) {
    if (n < 2) return false;
    ll fact = 1 % n;
    for (ll i = 1; i <= n - 1; i++) fact = fact * i % n;
    return fact == n - 1;
}

/* ===== (p-2)! mod p = 1(威尔逊推论) ===== */
ll factPMinus2(ll p) { return 1 % p; }

int main() {
    cout << invFermat(3, 11) << endl;                     // 4
    cout << qpow(2, reduceExp("1000000", 13), 13) << endl;  // 3
    cout << wilsonPrime(13) << " " << wilsonPrime(15) << endl;  // 1 0
    cout << factPMinus2(13) << endl;                      // 1
    return 0;
}

🔍 代码逐段解析 · 万能模板四件套

qpow(base, exp, p) —— 三参快速幂,res = 1 % p、base %= p 双保险。

invFermat(a, p) —— 一行 qpow(a, p-2, p),调用前必须自查"p 素数 + 互质"。

reduceExp(k, p) —— 字符串指数降幂:逐字符 (r*10 + digit) % (p-1),返回压缩后的小指数。

wilsonPrime(n) —— O(n) 阶乘判定,只适合小 n;1 % n 处理退化输入。

factPMinus2(p) —— 威尔逊推论的直接实现:(p−2)! ≡ 1,所以直接返回 1 % p。

main 演示 —— invFermat(3,11)=4;qpow(2, reduceExp("1000000",13), 13)=3;wilsonPrime(13)=1、wilsonPrime(15)=0。

易错点 —— ① reduceExp 模写成 p;② 用 wilsonPrime 判大数(TLE)。

🔑 考场三步走

① 模数是素数吗?是 → 费马可用;不是 → 换欧拉定理或 exgcd;
② 指数超大吗?大 → 先对 p − 1 取模降幂;
③ 见到 (p−1)! 或 (p−2)!?→ 立刻联想威尔逊定理的 −1 与 1。

②拓展案例
拓展1:求元素的阶(幂的循环长度)

问题:给定素数 p 与 gcd(a, p) = 1,求最小的正整数 k 使 a^k ≡ 1 (mod p)。

思路 费马推论

由费马小定理,这样的 k 一定存在且 k 整除 p − 1。证明:设 k 是最小的正周期,把 p − 1 除以 k 得 p − 1 = qk + r(0 ≤ r < k),则 a^r = a^(p−1) × (a^k)^(−q) ≡ 1,若 r > 0 就与 k 的最小性矛盾,故 r = 0。

因此只需枚举 p − 1 的所有约数,从小到大检查 a^d ≡ 1 是否成立,第一个成立的 d 就是答案。因子个数约为 O(√p) 级别。

C++求元素的阶 — 完整代码
#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; cin >> a >> p;
    vector<ll> divs;
    for (ll d = 1; d * d <= p - 1; d++)
        if ((p - 1) % d == 0) {
            divs.push_back(d);
            if (d != (p - 1) / d) divs.push_back((p - 1) / d);
        }
    sort(divs.begin(), divs.end());
    for (ll d : divs)
        if (qpow(a, d, p) == 1) { cout << d << endl; break; }
    return 0;
}
运行结果
输入: 3 7
输出: 6
(3 的幂序列 3,2,6,4,5,1,循环长度 6,恰好等于 p-1)

输入: 2 7
输出: 3
(2 的幂序列 2,4,1,2,4,1… 周期为 3,是 p-1=6 的真因子)

输入: 10 13
输出: 6
(10 ≡ -3,(-3)^6 = 729 ≡ 1 (mod 13),周期 6 是 12 的因子)
拓展2:从费马到 Miller-Rabin 素性测试

问题:威尔逊定理判素数太慢,有没有 O(log³n) 的替代方案?

费马素性测试 不充分

任取底数 a,若 n 是素数必有 a^(n−1) ≡ 1 (mod n)。但反过来不成立 —— 存在合数 n 使所有与它互质的 a 都满足这条式子,称为 Carmichael 数(最小的是 561 = 3 × 11 × 17)。所以费马测试只能"证伪",不能"证实"。

Miller-Rabin 工程标准

把 n − 1 写成 2^s × d(d 为奇数)。若 n 是素数,则对任意 a,要么 a^d ≡ 1,要么存在某个 r ∈ [0, s−1] 使 a^(2^r·d) ≡ −1。这不是必要条件(合数也可能通过),但通过的合数比例极低。取 2、3、5、7、11 等几个底数,在 64 位范围内可以做到确定正确。

C++Miller-Rabin 素性测试 — 完整代码
#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
typedef __int128 lll;

// 防溢出的快速幂(模数可达 10^18,需要用 __int128 中转)
ull qpow(ull b, ull e, ull p) {
    ull r = 1 % p; b %= p;
    while (e) {
        if (e & 1) r = (lll)r * b % p;
        b = (lll)b * b % p;
        e >>= 1;
    }
    return r;
}

bool isPrime(ull n) {
    if (n < 2) return false;
    for (ull p : {2ULL, 3ULL, 5ULL, 7ULL, 11ULL, 13ULL, 17ULL, 19ULL, 23ULL, 29ULL, 31ULL, 37ULL})
        if (n % p == 0) return n == p;

    ull d = n - 1, s = 0;
    while (!(d & 1)) { d >>= 1; s++; }

    for (ull a : {2ULL, 3ULL, 5ULL, 7ULL, 11ULL, 13ULL, 17ULL, 19ULL, 23ULL, 29ULL, 31ULL, 37ULL}) {
        if (a % n == 0) continue;
        ull x = qpow(a, d, n);
        if (x == 1 || x == n - 1) continue;
        bool ok = false;
        for (ull r = 1; r < s; r++) {
            x = (lll)x * x % n;
            if (x == n - 1) { ok = true; break; }
        }
        if (!ok) return false;
    }
    return true;
}

int main() {
    for (ull n : {561ULL, 1105ULL, 1000000007ULL, 1000000009ULL})
        cout << n << " -> " << (isPrime(n) ? "Prime" : "Composite") << endl;
    return 0;
}

🔍 代码逐段解析 · Miller-Rabin

qpow(ull, ull, ull) —— 模数可达 10¹⁸,两个 ull 相乘会溢出,所以用 (lll)r * b % p 走 __int128 中转。

小素数试除 —— 先用 2..37 的小素数试除并处理相等情形,挡掉大量小合数。

分解 n−1 = 2^s × d —— while (!(d & 1)) { d >>= 1; s++; } 把 2 的幂全部抽走,d 留成奇数。

主循环 —— 对每个底数 a:算 x = qpow(a, d, n);x 为 1 或 n−1 直接通过;否则平方至多 s−1 次,途中出现 n−1 则通过,都没出现判定为合数。

main —— 测试 561、1105(Carmichael 数,费马测试抓不住、MR 能抓住)与 10⁹+7、10⁹+9(真素数)。

易错点 —— ① 忘用 __int128 导致 10¹⁸ 级模乘溢出;② s−1 的循环上界写错;③ 底数 a ≥ n 时没跳过。

运行结果
561 -> Composite
(Carmichael 数!它满足 a^560 ≡ 1 (mod 561) 对所有 gcd(a,561)=1 成立,
  但 Miller-Rabin 的二次探测把它逮住了)

1105 -> Composite
(另一个 Carmichael 数)

1000000007 -> Prime
1000000009 -> Prime
(两个常用的竞赛大素数,各只需 12 次底数测试,耗时不到 1ms)
拓展3:用威尔逊定理算 (p−k)! mod p

问题:给定素数 p 与小整数 k(k < p),求 (p−k)! mod p。

思路 威尔逊 + 费马

把 (p−1)! 从高位往低位拆:(p−1)! = (p−1)(p−2)⋯(p−k) × (p−k−1)!。

模 p 下 (p−i) ≡ −i,所以前半部分等于 (−1)^k · k!。于是

(−1)^k · k! · (p−k−1)! ≡ −1 (mod p)

两边乘 (−1)^k · inv(k!),得到

(p−k−1)! ≡ (−1)^(k+1) · inv(k!) (mod p)

特例检验:k = 1 时 (p−2)! ≡ (−1)² · inv(1) = 1,与推论吻合;k = 2 时 (p−3)! ≡ (−1)³ · inv(2) = −inv(2)。

C++用威尔逊定理计算 (p−k−1)! — 完整代码
#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;
}

// 用威尔逊定理推出:(p-k-1)! ≡ (-1)^(k+1) * inv(k!) (mod p)
ll factFromWilson(ll p, ll k) {
    ll fk = 1 % p;
    for (ll i = 1; i <= k; i++) fk = fk * i % p;   // k!
    ll res = qpow(fk, p - 2, p);                    // inv(k!)
    if ((k + 1) % 2) res = (p - res) % p;             // (-1)^(k+1) 为负时取反
    return res;
}

// 暴力对照:直接算 (p-k-1)!(仅适合小 p)
ll brute(ll p, ll k) {
    ll f = 1 % p;
    for (ll i = 1; i <= p - k - 1; i++) f = f * i % p;
    return f;
}

int main() {
    for (ll p : {7LL, 11LL, 13LL})
        for (ll k = 1; k <= 3; k++)
            printf("p=%2lld k=%lld : 公式 %2lld  暴力 %2lld   %s\n", p, k,
                   factFromWilson(p, k), brute(p, k),
                   factFromWilson(p, k) == brute(p, k) ? "✓" : "✗");
    return 0;
}
运行结果
p= 7 k=1 : 公式  1  暴力  1   ✓
p= 7 k=2 : 公式  3  暴力  3   ✓
p= 7 k=3 : 公式  6  暴力  6   ✓
p=11 k=1 : 公式  1  暴力  1   ✓
p=11 k=2 : 公式  5  暴力  5   ✓
p=11 k=3 : 公式  2  暴力  2   ✓
p=13 k=1 : 公式  1  暴力  1   ✓
p=13 k=2 : 公式  6  暴力  6   ✓
p=13 k=3 : 公式 11  暴力 11   ✓

手工验算(p = 7):
  k=1 → (p-k-1)! = 5! = 120 ≡ 1 (mod 7),公式 (-1)^2 · inv(1!) = 1 ✓
  k=2 → (p-k-1)! = 4! =  24 ≡ 3 (mod 7),公式 (-1)^3 · inv(2!) = -4 ≡ 3 ✓
  k=3 → (p-k-1)! = 3! =   6 ≡ 6 (mod 7),公式 (-1)^4 · inv(6) = 6 ✓

注意:暴力法需要 O(p) 次乘法,p 达 10^9 时不可行;
     公式法只用 O(k + log p) 就能算出结果,这正是威尔逊定理的威力。

💡 拓展3 的关键:把下标写清楚

用同样的拆法还能得到另一条式子:把 (p−1)! 拆成"前 k−1 个因子"与 (p−k)! 的乘积,可得

(p−k)! ≡ (−1)^k · inv((k−1)!) (mod p)

两条式子只差一个因子 k,非常容易混。做题时先写清楚"乘到哪一项为止",再代入 k 的一个具体值验证(例如 p = 7, k = 1 时答案必须是 1),确认无误后再写代码。

这类由威尔逊定理派生的阶乘化简式,在初赛填空题和部分提高组数论题中经常出现,属于"知道就能秒杀、不知道就无从下手"的知识点。

③思考与启发

💡 三个值得深挖的问题

思考1:费马小定理的逆命题成立吗?即"若 a^(n−1) ≡ 1 (mod n) 对某个 a 成立,能否断定 n 是素数"?

查看参考答案

不成立。取 n = 341 = 11 × 31 是合数,但 2^340 ≡ 1 (mod 341) 成立,这样的 n 叫"以 2 为底的伪素数"。

更极端的反例:Carmichael 数 561 = 3 × 11 × 17,对所有与 561 互质的 a 都满足 a^560 ≡ 1 (mod 561)。最小三个 Carmichael 数是 561、1105、1729。

意义:这说明费马小定理只能作为"必要条件的检验" —— 不通过一定不是素数,通过了却可能是合数。要做到确定判定,需要 Miller-Rabin 的二次探测(见拓展2),或对 64 位范围取足够多的底数。

易错:在初赛选择题里把"a^(n−1) ≡ 1 (mod n) ⇒ n 为素数"当成真命题。

思考2:为什么说"指数对 p − 1 取模"和"底数对 p 取模"是两件完全不同的事?

查看参考答案

底数取模来自同余的可乘性:a^b mod p = (a mod p)^b mod p,因为乘法保持同余。这一步永远合法,与 p 是不是素数无关。

指数取模来自费马小定理:a^b mod p = a^(b mod (p−1)) mod p。这一步要求 p 是素数且 gcd(a, p) = 1,因为它的依据是"a^(p−1) ≡ 1",即幂运算具有周期 p − 1。

反例说明:p = 9(合数),a = 2。2^6 = 64 ≡ 1 (mod 9),似乎周期是 6 = p − 3 而不是 φ(9) = 6 —— 恰好这里 φ(9) = 6 也对,但换成 p = 8、a = 3,3^2 = 9 ≡ 1 (mod 8),周期是 2 而 p − 1 = 7,直接对 7 取模必然算错。

正确做法:模数不是素数时改用欧拉定理的 φ(m)(下一讲),且注意需要扩展欧拉定理处理不互质情形。

易错:看到"大指数"就条件反射地对 p 取模,忽略"周期长度是 p − 1"这个关键事实。

思考3:既然威尔逊定理给出了素数的充要条件,为什么工程上几乎不用它做素性判定?

查看参考答案

瓶颈在计算量。判定 n 需要算到 (n−1)!,即 n − 2 次乘法,复杂度 O(n)。n = 10⁹ 时约 10⁹ 次乘法,加上每步的取模运算,耗时以秒计。

对比方案:试除法 O(√n) ≈ 10⁵(n = 10⁹ 时快上万倍);线性筛 O(n) 但一次筛出全部素数;Miller-Rabin O(k log³n) 对 64 位整数只需微秒级。

威尔逊定理的真正价值:① 理论推导(比如证明某些同余式、化简阶乘表达式);② 与 (p−2)! ≡ 1 等推论配合,解决含阶乘的计数问题;③ 初赛选择题中考察"素数的充要条件"这一概念。

易错:把威尔逊定理写进"判断 n ≤ 10⁹ 是否素数"的程序里,导致 TLE 或中间结果溢出。

模块 05

学习检测

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

易错1

模数不是素数却用费马小定理

❌ 错误
inv = qpow(a, m - 2, m);(m = 8、a = 3)算出 3^6 mod 8 = 1,看着像答案,实际 3 在模 8 下的逆元是 3(3×3 = 9 ≡ 1),完全对不上。
✅ 正确
费马小定理要求 p 为素数。模数不确定时用扩展欧几里得:exgcd(a, m, x, y); x = (x % m + m) % m;;若 m 是合数但只要求逆元,exgcd 同样适用。
口诀:费马只认素数模,合数一律找扩欧!
易错2

底数是模数的倍数还去降幂

❌ 错误
p = 7、a = 14,直接用 a^(k mod (p-1)) mod p 算,得到 0^k' 的混乱结果;本应恒为 0(k > 0)。降幂的前提 gcd(a, p) = 1 被破坏。
✅ 正确
先判断 a % p == 0:若是,则 k > 0 时答案为 0,k = 0 时答案为 1 % p。确认 gcd(a, p) = 1 后再降幂。
口诀:底数先取模,为零不用降,答案直接零!
易错3

指数对 p 取模而不是 p − 1

❌ 错误
r = k % p; 再算 a^r。周期长度被错认为 p,答案必然不对。例:p = 13、a = 2、k = 1000000,正确余数是 1000000 mod 12 = 4,若写成 mod 13 得 12,结果差别巨大。
✅ 正确
r = k % (p - 1); 因为费马小定理给出的周期是 p − 1。字符串指数时写 r = (r * 10 + c - '0') % (p - 1)。
口诀:底数模 p,指数模 p 减一,千万别搞反!
易错4

用威尔逊定理做大范围素性判断

❌ 错误
对 n ≤ 10⁹ 的每个数都算一遍 (n−1)! mod n,复杂度 O(n²) 级别,必然 TLE;若中间量不取模还会溢出成负数。
✅ 正确
威尔逊定理只用于 小范围验证(n ≤ 20 做演示)与 理论推导。实际判定用试除 O(√n) 或筛法 O(n) 或 Miller-Rabin。
口诀:威尔逊能证素数,但阶乘太贵,只能小打小闹!
易错5

混淆 a^p ≡ a 与 a^(p−1) ≡ 1 的适用条件

❌ 错误
用形如 a^p ≡ a 的式子去推 inv(a) = a^(p−2),却不检查 a 是否被 p 整除。若 a ≡ 0,两边都是 0,约去 a 得到的是无意义的等式。
✅ 正确
记住分界:a^p ≡ a 只要求 p 素数(对 a ≡ 0 也成立);a^(p−1) ≡ 1 还要求 gcd(a, p) = 1。推导逆元公式必须用后者。
口诀:形式二要素数,形式一要素数加互质!
📋速查表
结论 / 公式内容前提条件
费马小定理 · 形式一a^(p−1) ≡ 1 (mod p)p 素数,gcd(a, p) = 1
费马小定理 · 形式二a^p ≡ a (mod p)仅需 p 素数
求逆元inv(a) = a^(p−2) mod pp 素数,gcd(a, p) = 1
降幂a^k ≡ a^(k mod (p−1))p 素数,gcd(a, p) = 1
循环节长度a 的阶整除 p − 1p 素数
威尔逊定理(p−1)! ≡ −1 (mod p) ⟺ p 素数p ≥ 2,充要条件
威尔逊推论(p−2)! ≡ 1 (mod p)p 素数
阶乘化简(p−k−1)! ≡ (−1)^(k+1)·inv(k!)p 素数,k < p
卢卡斯定理C(n,m) ≡ ∏ C(nᵢ, mᵢ) mod pp 素数
复杂度对照快速幂 O(log b) / 试除 O(√n) / 威尔逊 O(n)—
📌笔记卡
必记概念
  • 费马:a^(p−1) ≡ 1(p 素数 + 互质)
  • 费马形式二:a^p ≡ a(只要求素数)
  • 降幂:指数 mod (p−1)
  • 威尔逊:(p−1)! ≡ −1 ⟺ p 素数
  • 费马是欧拉定理在 m = p 时的特例
必背模板
  • 快速幂(六步循环)
  • invFermat(a, p) = qpow(a, p−2, p)
  • 字符串指数边读边模 p−1
  • wilsonPrime(n):累乘到 n−1 判 n−1
  • Lucas(n, m):递归拆 p 进制
考场提醒
  • 先问"模数是素数吗"
  • 再问"底数与模数互质吗"
  • 指数降幂用 p−1,不用 p
  • 威尔逊只做小范围验证
  • Miller-Rabin 才是工程方案
课后练习

分层练习作业

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

🟢 A组 · 基础(4题)

A1计算题

用费马小定理手算 5^2024 mod 11。

查看参考答案

思路:11 是素数,gcd(5, 11) = 1,周期为 10。把指数对 10 取模。

解答:2024 = 10 × 202 + 4,所以 5^2024 ≡ 5^4 = 625 (mod 11)。625 = 11 × 56 + 9,故答案为 9。

考点:费马小定理降幂,指数 mod (p−1)。

易错:把指数对 11 取模(得 2024 mod 11 = 0,误答 1);忘记 5^4 = 625 还要再取模。

A2编程题

输入素数 p 与整数 a,用费马小定理输出 a 的逆元。

查看参考答案

思路:逆元等于 a^(p−2) mod p,一次快速幂即可。

#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; cin >> a >> p;
    if (a % p == 0) { cout << "No Solution" << endl; return 0; }
    cout << qpow(a, p - 2, p) << endl;
    return 0;
}

🔍 代码逐段解析

qpow —— 三参快速幂。

main —— 特判 a % p == 0 输出 No Solution(0 无逆元);否则一行 qpow(a, p - 2, p) 完成。

易错点 —— ① 指数 p−1;② a 是 p 的倍数未特判。

考点:费马小定理求逆元的直接实现。

易错:指数写成 p−1;p 不是素数时结果错误;a 是 p 的倍数时未特判。

A3编程题

输入素数 p 与字符串指数 k(长度 ≤ 10³),输出 2^k mod p。

查看参考答案

思路:把 k 边读边对 p−1 取模得到 r,再用快速幂算 2^r mod p。

#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 p; string k; cin >> p >> k;
    ll r = 0;
    for (char c : k) r = (r * 10 + (c - '0')) % (p - 1);
    cout << qpow(2, r, p) << endl;
    return 0;
}

考点:字符串指数的降幂处理。

易错:对 p 取模而不是 p−1;用 stoll 转换导致溢出。

A4证明题

用威尔逊定理证明:p 为素数时 (p−2)! ≡ 1 (mod p)。

查看参考答案

证明:由威尔逊定理,(p−1)! ≡ −1 (mod p)。

另一方面 (p−1)! = (p−1) × (p−2)!,而 p − 1 ≡ −1 (mod p),所以

(p−1)! ≡ (−1) × (p−2)! (mod p)。

代入得 (−1)(p−2)! ≡ −1,两边同乘 −1,得 (p−2)! ≡ 1 (mod p)。证毕。

验证:p = 7 时 (p−2)! = 5! = 120 = 7 × 17 + 1 ≡ 1 ✓

考点:威尔逊定理的推论,以及"把负数写成 p−1"的常规技巧。

易错:忘记 p − 1 ≡ −1 这一步,直接写成 (p−1)! ≡ (p−1)(p−2)! 而不敢化简。

🟡 B组 · 提高(4题)

B1综合编程

给定素数 p 与 a,求最小的正整数 k 使 a^k ≡ 1 (mod p)。(提示:k 整除 p−1)

查看参考答案

思路:枚举 p − 1 的所有约数,从小到大找第一个满足 a^d ≡ 1 的 d。

#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; cin >> a >> p;
    vector<ll> d;
    for (ll i = 1; i * i <= p - 1; i++)
        if ((p - 1) % i == 0) { d.push_back(i); if (i != (p-1)/i) d.push_back((p-1)/i); }
    sort(d.begin(), d.end());
    for (ll x : d) if (qpow(a, x, p) == 1) { cout << x << endl; break; }
    return 0;
}

样例:输入 2 7 输出 3;输入 3 7 输出 6。

考点:"阶整除 p−1"这个费马推论,以及约数枚举技巧。

易错:从 1 到 p−1 逐个暴力试(p 大时超时);忘记枚举约数时要同时加入 i 和 (p−1)/i。

B2综合编程

用卢卡斯定理求 C(n, m) mod p,其中 n, m ≤ 10¹⁸,p ≤ 10⁵ 为素数。

查看参考答案

思路:递归把 n、m 拆成 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;
}
ll comb(ll a, ll b) {
    if (b > a) return 0;
    ll num = 1, den = 1;
    for (ll i = 1; i <= b; i++) { num = num * ((a - i + 1) % p) % p; den = den * i % p; }
    return num * qpow(den, p - 2) % p;
}
ll lucas(ll n, ll m) {
    if (m == 0) return 1 % p;
    return comb(n % p, m % p) * lucas(n / p, m / p) % p;
}
int main() {
    ll n, m; cin >> n >> m >> p;
    cout << lucas(n, m) << endl;
    return 0;
}

样例:输入 10 3 7 输出 1;输入 10 3 5 输出 0。

🔍 代码逐段解析

comb(a, b) —— 小组合数:分子分母逐项累乘取模,最后费马逆元除 b!。

lucas(n, m) —— comb(n%p, m%p) * lucas(n/p, m/p) % p 递归拆 p 进制;m == 0 返回 1 % p。

易错点 —— ① 递归无终止条件;② comb 漏取模;③ 全局 p 忘读入。

考点:卢卡斯定理 + 费马小定理求小组合数的逆元。

易错:递归终止条件漏写(m = 0 时返回 1 % p);小组合数里忘记取模导致 long long 溢出。

B3分析题

已知 p = 1000000007 为素数,求 3^(10⁵) mod p 与 3^(10⁵ + (p−1)) mod p 的关系,并解释原因。

查看参考答案

结论:两者相等。

推导:设 3^(10^5) ≡ X,则 3^(10^5 + (p−1)) = 3^(10^5) × 3^(p−1) ≡ X × 1 = X (mod p),其中用到了费马小定理 3^(p−1) ≡ 1。

一般化:只要 gcd(a, p) = 1,a^k mod p 就是关于 k 的周期函数,周期整除 p − 1。因此"指数加 p − 1"对结果毫无影响。

启示:这正是降幂公式的理论基础 —— 任何指数都可以先对 p − 1 取模再计算。对于 p 有 10 位的题目,这能省下 10¹⁰ 级别的乘法。

易错:以为"指数加一个很大的数结果一定会变",本质上忽略了幂运算的周期性。

B4综合编程

判断 n(2 ≤ n ≤ 10⁷)是否为素数,要求同时给出威尔逊判定与试除法的结果并比较效率。

查看参考答案

思路:威尔逊法累乘 O(n),试除法只到 √n(O(√n))。对 n = 10⁷,前者约 10⁷ 次乘法,后者约 3×10³ 次,差距超过三个数量级。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
bool wilson(ll n) {
    if (n < 2) return false;
    ll f = 1 % n;
    for (ll i = 1; i <= n - 1; i++) f = f * i % n;
    return f == n - 1;
}
bool trial(ll n) {
    if (n < 2) return false;
    for (ll i = 2; i * i <= n; i++) if (n % i == 0) return false;
    return true;
}
int main() {
    ll n; cin >> n;
    clock_t t1 = clock();
    bool a = wilson(n);
    double d1 = double(clock() - t1) / CLOCKS_PER_SEC;
    clock_t t2 = clock();
    bool b = trial(n);
    double d2 = double(clock() - t2) / CLOCKS_PER_SEC;
    printf("威尔逊:%s 用时 %.4f s\n", a ? "素数" : "合数", d1);
    printf("试除法:%s 用时 %.4f s\n", b ? "素数" : "合数", d2);
    return 0;
}

样例输出(n = 9999991,素数):

威尔逊:素数 用时 0.0218 s
试除法:素数 用时 0.0000 s
结论:两者判定一致,但试除法快了几百倍。

考点:威尔逊定理的正确性 + 复杂度意识(定理好用 ≠ 算法好用)。

易错:把威尔逊法写成 n 达 10⁹ 的判定题(必然 TLE);忘记中间取模导致阶乘溢出。

🔴 C组 · 挑战(4题)

C1初赛选择题

下列关于费马小定理与威尔逊定理的说法,正确的有几个?
① 若 p 为素数,则对任意整数 a 都有 a^p ≡ a (mod p)
② 若 a^(n−1) ≡ 1 (mod n) 对某个 a 成立,则 n 必为素数
③ (p−1)! ≡ −1 (mod p) 是 p 为素数的充要条件
④ p 为素数时 (p−2)! ≡ 1 (mod p)

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

查看参考答案

答案:C(3 个)

①正确:这正是费马小定理的第二形式,对 a ≡ 0 也成立(0^p = 0 ≡ a)。

②错误:反例 n = 341 = 11 × 31(以 2 为底的伪素数),更强的反例是 Carmichael 数 561。

③正确:威尔逊定理给出的正是充要条件。

④正确:由 (p−1)! = (p−1)(p−2)! ≡ (−1)(p−2)! ≡ −1,得 (p−2)! ≡ 1。

考点:费马小定理的逆命题不成立、威尔逊定理的充要性、威尔逊推论。

易错:把伪素数当成"反例不存在",误选 ② 为正确。

C2构造与证明

设 p 为素数,证明:对任意整数 a、b,(a + b)^p ≡ a^p + b^p (mod p)。这在一般情况下的二项式展开里为什么不成立?对合数模数举出反例。

查看参考答案

证明:由二项式定理,(a+b)^p = Σ C(p, k) a^k b^(p−k)(k 从 0 到 p)。

当 0 < k < p 时,C(p, k) = p! / (k!(p−k)!),分子含因子 p,而分母不含(因为 k < p 且 p−k < p,且 p 为素数),所以 p | C(p, k),即 C(p,k) ≡ 0 (mod p)。

于是所有中间项全部被 p 整除而消失,只剩 k = 0 与 k = p 两项,得到 (a+b)^p ≡ a^p + b^p (mod p)。∎

合数反例:取 p = 4,a = b = 1。左边 (1+1)^4 = 16 ≡ 0 (mod 4);右边 1^4 + 1^4 = 2。0 ≠ 2,不成立。

意义:这个性质叫"模素数下的 Frobenius 同态",是卢卡斯定理证明的关键一步 —— 由它可得 (1 + x)^p ≡ 1 + x^p (mod p)。

易错:以为对任意模数都成立;或者在证明"中间项被 p 整除"时没有说明 p 是素数导致分母不含因子 p。

C3综合编程

给定素数 p(3 ≤ p ≤ 10⁹)与整数 k(1 ≤ k ≤ 10⁵),用威尔逊定理求 (p−k−1)! mod p。

查看参考答案

思路:由威尔逊定理与 (p−i) ≡ −i 可得 (p−k−1)! ≡ (−1)^(k+1) · inv(k!) (mod p)。k 只有 10⁵,直接算 k! 与它的逆元即可,无需遍历到 p。

#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 p, k; cin >> p >> k;
    ll fk = 1 % p;
    for (ll i = 1; i <= k; i++) fk = fk * i % p;      // k!
    ll res = qpow(fk, p - 2, p);                       // inv(k!) by Fermat
    if ((k + 1) % 2) res = (p - res) % p;                // (-1)^(k+1)
    cout << res << endl;
    return 0;
}

样例:输入 7 2 输出 3(4! = 24 ≡ 3);输入 13 3 输出 11(9! = 362880 ≡ 11)。

🔍 代码逐段解析

算 k! —— 循环 k 次(k ≤ 10⁵),逐项 fk * i % p。

inv(k!) —— qpow(fk, p - 2, p) 费马逆元,p 是素数的前提。

符号因子 —— if ((k + 1) % 2) res = (p - res) % p:k 为奇数时 (−1)^(k+1) = −1,把 res 变成 p − res。

易错点 —— ① 符号写成 (−1)^k;② p ≤ 3 边界下 (p − res) 需再 % p 防止出现 p 本身。

考点:威尔逊定理的阶乘化简、费马逆元、符号因子 (−1)^(k+1)。

易错:符号因子写成 (−1)^k;把 k! 算成 p−k 或其它下标;k 较大时未逐项取模。

C4算法设计

设计一个算法:给定素数 p 与 a(gcd(a,p)=1),求 a 的阶 k(最小正周期)。要求复杂度优于 O(p)。

查看参考答案

思路:由费马小定理,k | (p−1)。可以从 k = p − 1 出发,不断尝试"除以素因子":对 p − 1 的每个素因子 q,若 a^(k/q) ≡ 1,就把 k 缩小为 k/q,重复直到不可再缩。

为什么正确:真正的最小周期一定整除 p − 1。若 k/q 仍使 a 的幂为 1,说明 q 这个因子是多余的,可以去掉;把 p − 1 的所有素因子都试过一遍后剩下的就是最小周期。

复杂度:需要对 p − 1 分解素因子(O(√p)),之后对每个素因子做 O(log p) 次快速幂,总复杂度 O(√p + ω(p−1) · log p · log p),远优于 O(p)。

#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; cin >> a >> p;
    ll k = p - 1, m = p - 1;
    for (ll q = 2; q * q <= m; q++) {
        if (m % q) continue;
        while (m % q == 0) m /= q;
        while (k % q == 0 && qpow(a, k / q, p) == 1) k /= q;
    }
    if (m > 1) {
        while (k % m == 0 && qpow(a, k / m, p) == 1) k /= m;
    }
    cout << k << endl;
    return 0;
}

样例:输入 2 7 输出 3;输入 3 7 输出 6;输入 10 13 输出 6。

考点:费马小定理的推论(阶整除 p−1)+ 质因数分解 + 快速幂。

易错:分解 m 时忘记处理最后剩下的 m > 1 的大素因子;把 k % q == 0 的判断漏掉;用暴力从 1 枚举到 p−1。

🏆 洛谷原题精选(12道)

以下 12 道题围绕"费马小定理 / 威尔逊定理 / 降幂 / 素性判定 / 组合取模"展开,按难度从普及- 到提高排列。

1
P1226
【模板】快速幂||取余运算普及-
2
P3383
【模板】线性筛素数普及-
3
P3811
【模板】乘法逆元普及+
4
P2613
【模板】有理数取余普及+
5
P1835
素数密度普及+
6
P3390
【模板】矩阵快速幂普及+
7
P1082
同余方程提高
8
P3807
【模板】卢卡斯定理提高
9
P2822
组合数问题提高
10
P1313
计算系数提高
11
P3197
[SDOI2008] 越狱提高
12
P2480
[SDOI2010] 古代猪文提高

提示:第 12 题需要综合运用费马小定理、卢卡斯定理与中国剩余定理,是本讲知识的"最终 Boss",建议在学完 s6-06 后再挑战。