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

欧拉定理 与 欧拉函数

费马小定理只认素数,欧拉定理把它推广到任意模数;而连接两者的桥梁,就是"1 到 n 中有多少个数与 n 互质"这个朴素的问题 —— 欧拉函数 φ(n)。

6 大模块 O(n) 欧拉筛 3 个案例 扩展欧拉定理降幂 12 道洛谷原题
本讲 · 第 45/52 讲欧拉定理与欧拉函数
模块 01

情景导航

从"队伍里能看到几个人"这个几何问题出发,发现它背后藏着一个纯数论的函数。

故事教官的仪仗队

军训汇报表演,教官让 N × N 个同学站成一个方阵,每个人占一个格子,横竖间距都相等。

教官站在方阵的左上角,对第一排的小 Q 说:"你数一数,从我这里能看见多少个同学?"

小 Q 愣了:这不是把整支队伍都数一遍吗?N 最大可以到 40000,四万个格子一个个数,眼睛都要花了。

教官笑着说:"站在我斜后方的同学会被前面的人挡住,看不见。你看,只有在横竖坐标互质的位置上的同学才不会被挡。"

小 Q 恍然大悟:把教官的位置设为 (0, 0),某个同学在 (x, y)。如果 gcd(x, y) = d > 1,那么这个同学就恰好落在 (x/d, y/d) 这个同学的同一条射线上,被挡住了;反之若 gcd(x, y) = 1,这条射线上就没有更近的人。

于是问题变成了:在 0 ≤ x, y ≤ N−1 的网格中,有多少个点满足 gcd(x, y) = 1?

答案 = 1 + 2 × Σ φ(i)   (i 从 1 到 N−1)

而式子里的 φ,正是本讲的主角 —— 欧拉函数:它数的是"1 到 i 中有多少个数与 i 互质"。

💡 为什么这个问题这么重要

欧拉函数 φ(n) 看起来只是"数数互质的个数",却在数论里无处不在:欧拉定理用它给出幂运算的周期,RSA 加密用它构造公私钥,莫比乌斯反演用它处理 gcd 求和,连"约分后的最简分数有多少个"都靠它回答。

更重要的是,欧拉定理是费马小定理的推广:当模数 m 是素数时 φ(m) = m − 1,欧拉定理立刻退化成上一讲学过的费马小定理。所以本讲可以看作"把上一讲的条件放宽"。

而扩展欧拉定理进一步放宽了"必须互质"的要求,用 +φ(m) 的技巧处理任意底数 —— 这是处理"超大指数取模"类题目的终极武器。

①生活类比
🔦

手电筒照格子 = 可见性

光从原点射出,每条射线上只有最近的那个格子能被照亮,其余都被挡住。

→ gcd(x,y) = 1
🎟️

检票 = 筛掉"不互质"

1..n 里的人排队检票,只要和 n 有公因数就被拦下来,通过的人数就是 φ(n)。

→ 容斥筛法
🍕

切蛋糕 = 积性

把蛋糕按互质的两块分开切,配料比例互不影响 —— 这正是"积性函数"的直觉。

→ φ(ab) = φ(a)φ(b)
🔐

RSA 钥匙 = 欧拉定理

公钥加密后,用私钥能还原,靠的就是"乘 e·d 次方等于转了整数圈"。

→ a^φ(n) ≡ 1
②核心定义

欧拉函数 φ(n):1 到 n 中与 n 互质的整数的个数。约定 φ(1) = 1。例如 φ(1) = 1、φ(2) = 1、φ(6) = 2(只有 1 和 5)、φ(12) = 4(1, 5, 7, 11)。

积性函数:若 gcd(a, b) = 1 时总有 f(ab) = f(a)·f(b),则称 f 为积性函数。欧拉函数是积性函数。

欧拉定理:若 gcd(a, m) = 1,则 a^φ(m) ≡ 1 (mod m)。

扩展欧拉定理:对任意 a 与正整数 m,当 b ≥ φ(m) 时,a^b ≡ a^(b mod φ(m) + φ(m)) (mod m)。注意这里不需要互质。

线性筛(欧拉筛):用"每个合数只被它的最小质因子筛去一次"的方式在 O(n) 内求出素数表,同时可以顺带算出 φ、μ 等积性函数值。

📌 本讲核心知识地图

板块核心内容对应大纲
欧拉函数1..n 中与 n 互质的个数,φ(1)=1【7】欧拉函数
性质φ(p)=p−1、φ(p^k)=p^k−p^(k−1)、积性【7】欧拉函数
计算式φ(n) = n × ∏(1 − 1/pᵢ)【7】欧拉函数
求法一分解质因数单点求,O(√n)【7】欧拉函数
求法二欧拉筛(线性筛)打表,O(n)【7】欧拉函数
欧拉定理gcd(a,m)=1 时 a^φ(m) ≡ 1【7】欧拉定理
扩展欧拉定理b ≥ φ(m) 时 a^b ≡ a^(b mod φ(m) + φ(m))【7】欧拉定理
典型应用可见点计数、Σgcd、RSA、大指数降幂【7】欧拉定理和欧拉函数
③核心口诀
🔑 欧拉函数

互质个数叫 φ,质因子筛一遍;
先乘 n 再乘 (1 − 1/p),积性只在互质时用。

🔑 欧拉定理

只要互质就能用,指数周期是 φ(m);
m 取素数时 φ(m) = m − 1,退化成费马小定理。

🔑 扩展欧拉定理

不互质也别慌,指数加个 φ(m);
前提是 b 足够大(b ≥ φ(m)),加完再快速幂。

模块 02

知识探究

先算清楚 φ 的值与性质,再拿下两种求法,最后用欧拉定理与扩展欧拉定理把幂运算彻底打通。

1欧拉函数与它的四条性质 基础

定义:φ(n) = |{ k : 1 ≤ k ≤ n, gcd(k, n) = 1 }|,并约定 φ(1) = 1。

1
🌾
素数

φ(p) = p − 1

2
📚
素数幂

φ(p^k) = p^k − p^(k−1)

3
🧩
积性

gcd(a,b)=1 → φ(ab)=φ(a)φ(b)

4
➕
约数求和

Σ(d|n) φ(d) = n

📐 性质逐一说明

性质 1:φ(p) = p − 1(p 为素数)。1 到 p 中除了 p 本身,其余 p − 1 个数都与 p 互质。

性质 2:φ(p^k) = p^k − p^(k−1)。1 到 p^k 中,与 p^k 不互质的数恰好是 p 的倍数:p, 2p, 3p, …, p^(k−1)·p,共有 p^(k−1) 个。所以互质的个数是 p^k − p^(k−1) = p^(k−1)(p − 1)。

性质 3:积性。若 gcd(a, b) = 1,则 φ(ab) = φ(a)φ(b)。注意只在互质时成立,例如 φ(4) = 2 而 φ(2)·φ(2) = 1,因为 gcd(2, 2) ≠ 1。

性质 4:Σ_{d|n} φ(d) = n。证明思路:按 gcd(i, n) 的值把 1..n 分成若干组,gcd(i, n) = d 的数的个数恰好是 φ(n/d),把所有 d | n 求和即得 n。

✅ 积性函数的正确用法

12 = 4 × 3,gcd(4, 3) = 1,所以 φ(12) = φ(4)·φ(3) = 2 × 2 = 4 ✓

36 = 4 × 9,gcd(4, 9) = 1,所以 φ(36) = φ(4)·φ(9) = 2 × 6 = 12 ✓

关键:拆成互质的两部分,通常拆成不同素因子的幂。

❌ 把积性当成"完全积性"

φ(4) = 2,但 φ(2)·φ(2) = 1 × 1 = 1 ≠ 2 ✗

φ(9) = 6,但 φ(3)·φ(3) = 2 × 2 = 4 ≠ 6 ✗

原因:φ 是积性而非完全积性,两个因子必须互质才行。

n与 n 互质的数φ(n)说明
111约定值
2112 − 1
31, 223 − 1
41, 324 − 2
61, 52φ(2)φ(3) = 1×2
81, 3, 5, 748 − 4
91, 2, 4, 5, 7, 869 − 3
121, 5, 7, 114φ(4)φ(3) = 2×2
36(共 12 个)12φ(4)φ(9) = 2×6

下表是 φ(1) 到 φ(12) 的取值,格子会依次高亮:

n=1
1
2
1
3
2
4
2
5
4
6
2
7
6
8
4
9
6
10
4
11
10
12
4

观察规律:素数位置(2,3,5,7,11)的 φ 值恰好是它减 1;合数的 φ 值通常更小。

2计算式 φ(n) = n × ∏(1 − 1/pᵢ) 与容斥直觉 高频

把 n 分解成质因数的幂:n = p₁^a₁ × p₂^a₂ × … × p_k^a_k,则

🔑 欧拉函数计算式

φ(n) = n × (1 − 1/p₁) × (1 − 1/p₂) × … × (1 − 1/p_k)

注意式子中只出现不同的质因子 pᵢ,指数 aᵢ 完全不参与 —— 这是最容易记错的一点。

📐 容斥角度理解这个式子

1 到 n 中"与 n 不互质"的数,就是那些被某个质因子 pᵢ 整除的数。用容斥原理逐个剔除:

· 先剔除 p₁ 的倍数:共 n/p₁ 个,剩下 n − n/p₁ = n(1 − 1/p₁) 个;

· 再剔除 p₂ 的倍数:在剩下的里面有一部分已被剔过(即同时是 p₁p₂ 倍数的),这一部分恰好按比例占 1/p₂,所以剩下 n(1 − 1/p₁)(1 − 1/p₂) 个;

· 依此类推,每引入一个新的质因子就乘一个 (1 − 1/pᵢ),最后得到 n × ∏(1 − 1/pᵢ)。

为什么可以"按比例"直接乘?因为在 [1, n] 这个完整区间里,pᵢ 的倍数是均匀分布的,而"已经是 p₁…p_(i−1) 的倍数"的那些数在 pᵢ 的倍数中同样均匀分布(这正是中国剩余定理保证的),所以比例可以连乘。

🎬 交互演示:用容斥筛法一步步算出 φ(12) = 4
1分解质因数:12 = 2² × 3,不同的质因子只有 2 和 3。指数 2 在后面的计算里完全用不到。
2从 n 出发:令 res = 12,准备逐个质因子做"剔除"。
3剔除质因子 2 的倍数:1..12 中是 2 的倍数有 12/2 = 6 个(2,4,6,8,10,12),它们与 12 不互质。res = 12 − 6 = 6,等价于乘 (1 − 1/2)。
4剔除质因子 3 的倍数:当前剩下的 6 个数是 {1,3,5,7,9,11},其中 3 的倍数有 3 和 9 两个。res = 6 − 2 = 4,等价于乘 (1 − 1/3)。
5得到结果:剩下的数是 {1, 5, 7, 11},共 4 个。所以 φ(12) = 4。验算:12 × 1/2 × 2/3 = 4 ✓
6对照另一个角度:12 = 4 × 3 且 gcd(4,3) = 1,用积性得 φ(12) = φ(4)·φ(3) = 2 × 2 = 4,两条路殊途同归。
💡 代码实现时为了保证整除,写成 res = res / p * (p - 1):先除以 p 再乘 (p − 1),这样每一步结果都是整数,绝不会丢精度。
C++单点求 φ(n):分解质因数,O(√n)
// 返回 φ(n),时间复杂度 O(√n)
long long phi(long long n) {
    long long res = n;                 // 从 n 出发逐个质因子约分
    for (long long p = 2; p * p <= n; p++) {
        if (n % p == 0) {
            res = res / p * (p - 1);    // 先除后乘,保证整除不丢精度
            while (n % p == 0) n /= p;  // 把质因子 p 除干净
        }
    }
    if (n > 1) res = res / n * (n - 1);  // 剩下的大质因子
    return res;
}
易错提醒:res = res / p * (p - 1) 的顺序不能颠倒。写成 res * (p - 1) / p 时中间结果可能不整除,虽然整数除法会"向下取整"看似也能得到正确结果,但当 res 很大时 res * (p-1) 会溢出 long long,而先除后乘永远不会溢出。
3欧拉筛(线性筛):O(n) 求出整张 φ 表 必背模板

如果需要对 1 到 n 的所有数都求 φ,逐个分解质因数是 O(n√n),n = 10⁶ 时会超时。线性筛可以在 O(n) 内一次算出整张表。

📐 核心思想:每个合数只被它的最小质因子筛掉一次

遍历 i 从 2 到 n,维护一张素数表 prime[]。对每个 i,枚举已有的素数 p,把 i × p 标记为合数。一旦 i % p == 0 就立刻 break。

为什么 break 能保证线性?当 i % p == 0 时,p 是 i 的最小质因子,那么 i × p'(p' 是更靠后的素数)的最小质因子仍是 p,应该在后面由 (i × p'/p) 那一轮筛掉。提前 break 保证每个合数只在"最小质因子"那一轮被筛,总操作次数 O(n)。

🔑 顺带维护 φ 的两个分支

设 p = prime[j],考虑合数 x = i × p:

分支一:i % p == 0(p 是 i 的质因子,即 p | x 且 p² | x)。此时 x 的质因子集合与 i 完全相同,所以 φ(x) = φ(i) × p。
直觉:φ 只与质因子集合有关,多乘了一个已存在的因子 p,相当于倍数整体放大 p 倍。

分支二:i % p != 0(p 是 x 的最小质因子且 gcd(i, p) = 1)。此时可用积性:φ(x) = φ(i) × φ(p) = φ(i) × (p − 1)。

边界:phi[1] = 1;若 i 是素数则 phi[i] = i − 1。

C++欧拉筛求 φ 表 — 标准模板
const int MAXN = 1000005;
int phi[MAXN], prime[MAXN], cnt = 0;
bool vis[MAXN];

void buildPhi(int n) {
    phi[1] = 1;                       // 边界约定
    for (int i = 2; i <= n; i++) {
        if (!vis[i]) {                  // i 是素数
            prime[++cnt] = i;
            phi[i] = i - 1;
        }
        for (int j = 1; j <= cnt && i * prime[j] <= n; j++) {
            int x = i * prime[j];
            vis[x] = true;
            if (i % prime[j] == 0) {      // p 已经是 i 的因子
                phi[x] = phi[i] * prime[j];
                break;                    // 保证每个数只被最小质因子筛一次
            } else {                        // gcd(i, p) = 1,用积性
                phi[x] = phi[i] * (prime[j] - 1);
            }
        }
    }
}
方法复杂度适用场景特点
分解质因数O(√n) / 次只求一个 φ(n)代码短,不需要额外空间
埃氏筛法O(n log log n)求整张表写法简单,常数小
欧拉筛(线性筛)O(n)求整张表 + 需要素数表顺带得到素数,可扩展求 μ 等
4欧拉定理:把费马小定理推广到任意模数 核心

欧拉定理:若 gcd(a, m) = 1,则 a^φ(m) ≡ 1 (mod m)。

📐 证明:与费马小定理完全平行的"重排"论证

第 1 步:取简化剩余系。把所有与 m 互质、且落在 [1, m] 中的数收集起来,记为 r₁, r₂, …, r_φ(m)。这 φ(m) 个数就是模 m 的一个简化剩余系。

第 2 步:全体乘 a。考虑 a·r₁, a·r₂, …, a·r_φ(m)。

第 3 步:它们仍然与 m 互质。因为 gcd(a, m) = 1 且 gcd(rᵢ, m) = 1,乘积与 m 的 gcd 仍为 1。

第 4 步:它们两两不同余。若 a·rᵢ ≡ a·rⱼ (mod m),由 gcd(a, m) = 1 可约去 a,得 rᵢ ≡ rⱼ,故 i = j。

第 5 步:这是一次重排。φ(m) 个与 m 互质、两两不同余的余数类,正好就是原来那 φ(m) 个,只是顺序变了。

第 6 步:乘积相等并约去。两边全体相乘:a^φ(m) · (r₁r₂⋯r_φ(m)) ≡ r₁r₂⋯r_φ(m) (mod m)。由于每个 rᵢ 都与 m 互质,整体乘积也与 m 互质,有逆元可约去,得到 a^φ(m) ≡ 1 (mod m)。∎

退化为费马小定理:当 m = p 为素数时,1..p 中除 p 之外全都与 p 互质,φ(p) = p − 1,于是 a^(p−1) ≡ 1 (mod p)。

mφ(m)与 m 互质的 a(举例)验证 a^φ(m) mod m
7(素数)633^6 = 729 ≡ 1 ✓
8433^4 = 81 ≡ 1 ✓
8455^4 = 625 ≡ 1 ✓
12455^4 = 625 ≡ 1 ✓
12477^4 = 2401 ≡ 1 ✓
10433^4 = 81 ≡ 1 ✓
1042gcd(2,10)≠1,定理不适用
🔑 使用前必查三件事

① 模数 m 是多少?② φ(m) 等于多少?③ gcd(a, m) 是不是 1?
第三条是欧拉定理的死穴 —— 不互质时定理直接失效,必须用扩展形式。

5扩展欧拉定理:不互质也能降幂 终极武器

欧拉定理要求 gcd(a, m) = 1,但很多题目里底数恰好与模数不互质。扩展欧拉定理解决了这个痛点:

1
📏
b < φ(m) 时

a^b mod m 直接算,不能降幂

2
🔁
b ≥ φ(m) 时

a^(b mod φ(m) + φ(m)) mod m

📐 为什么要加一个 φ(m)?

欧拉定理告诉我们 a^φ(m) ≡ 1,但前提是互质。不互质时 a^φ(m) ≢ 1,单纯的"对 φ(m) 取模"就不再等价。

然而,"模 m 意义下的幂序列"最终仍然是周期性的:从某一项之后,它会进入一个长度整除 φ(m) 的循环。为了保证"往回退"时不掉进前面那段非周期的部分,我们额外加上一个 φ(m) —— 这样就一定落在循环段内部,从而保证取模运算合法。

关键细节:只有当 b ≥ φ(m) 时才允许加 φ(m)。如果 b 本身就小于 φ(m),那么 a^b 可能还处在"周期之前的特殊段",此时必须老老实实直接算。

a^b mod m = a^b (b < φ(m))   |   a^(b mod φ(m) + φ(m)) (b ≥ φ(m))

✅ 正确:加 φ(m)

求 2^5 mod 12。φ(12) = 4,5 ≥ 4,所以 r = 5 mod 4 + 4 = 5。

计算 2^5 = 32 ≡ 8 (mod 12)。答案 8 ✓

❌ 错误:只取 b mod φ(m)

同一题若写成 r = 5 mod 4 = 1,算 2^1 = 2,得到 答案 2 ✗。

错因:2 与 12 不互质,欧拉定理不成立,不能单纯降幂。

🔍 再看一个"互质"的对照例

求 3^1000 mod 12。这里 gcd(3, 12) = 3 ≠ 1,同样属于不互质情形,必须用扩展形式。

φ(12) = 4,1000 ≥ 4,所以 r = 1000 mod 4 + 4 = 0 + 4 = 4。计算 3^4 = 81 ≡ 9(81 − 72 = 9)。

而 3^1000 一定是 9 的幂次,3^2 = 9、3^4 = 81 ≡ 9、3^6 = 729 ≡ 9 …… 从 n ≥ 2 起恒为 9,答案确实是 9 ✓

注意:如果错误地只取 1000 mod 4 = 0,算出 3^0 = 1,就完全错了。这正是"必须加 φ(m)"的价值所在。

STEP 1算 φ(m)
STEP 2字符串读 b
STEP 3边读边取模
STEP 4判 b≥φ(m) 加 φ(m)
STEP 5快速幂求值
五步走完,一个上万位的指数就被压缩成了不超过 2φ(m) 的小整数
幂序列的"暂态 + 循环"结构(以 2^n mod 12 为例) n=1: 2 n=2: 4 n=3: 8 n=4: 4 n=5: 8 循环段(长度整除 φ(m)=4) 暂态 加上 φ(m) 的作用:把指数"推"进循环段,保证取模运算合法
案例案例1:欧拉筛一次求出整张 φ 表 模板

题目:给定 n(n ≤ 10⁶),输出 φ(1), φ(2), …, φ(n),每个一行,并求出它们的总和。

🔍 分析过程

第1步:确定算法 — 要求整张表,逐个分解是 O(n√n) ≈ 10⁹,会超时;改用欧拉筛 O(n)。

第2步:边界 — phi[1] = 1,这是约定值,不要漏。

第3步:素数判断 — 若 i 未被标记,则 i 是素数,记入素数表并令 phi[i] = i − 1。

第4步:筛合数 — 对每个素数 p,处理 x = i × p。若 i % p == 0 则 φ(x) = φ(i)·p 并 break;否则 φ(x) = φ(i)·(p−1)。

第5步:类型 — 前缀和可能达 10⁶ × 10⁶,需要 long long 存总和。

复杂度:O(n) 时间、O(n) 空间,n = 10⁶ 时约 0.05 秒。

C++欧拉筛求 φ 表 — 完整代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int MAXN = 1000005;
int phi[MAXN], prime[MAXN], cnt = 0;
bool vis[MAXN];

void buildPhi(int n) {
    phi[1] = 1;                        // 约定 φ(1) = 1
    for (int i = 2; i <= n; i++) {
        if (!vis[i]) {                   // i 是素数
            prime[++cnt] = i;
            phi[i] = i - 1;              // φ(素数) = 素数 - 1
        }
        for (int j = 1; j <= cnt && (ll)i * prime[j] <= n; j++) {
            int x = i * prime[j];
            vis[x] = true;
            if (i % prime[j] == 0) {
                phi[x] = phi[i] * prime[j];      // p 已在 i 中,质因子集合不变
                break;                            // 保证线性复杂度
            }
            phi[x] = phi[i] * (prime[j] - 1);   // gcd(i,p)=1,用积性
        }
    }
}

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

    ll sum = 0;
    for (int i = 1; i <= n; i++) {
        printf("phi[%d] = %d\n", i, phi[i]);
        sum += phi[i];
    }
    printf("总和 = %lld\n", sum);
    return 0;
}
运行结果
输入: 12
输出: phi[1] = 1
      phi[2] = 1
      phi[3] = 2
      phi[4] = 2
      phi[5] = 4
      phi[6] = 2
      phi[7] = 6
      phi[8] = 4
      phi[9] = 6
      phi[10] = 4
      phi[11] = 10
      phi[12] = 4
      总和 = 46

手工核对 φ(12):1..12 中与 12 互质的是 1, 5, 7, 11,共 4 个 ✓
       φ(11):11 是素数,φ(11) = 10 ✓
       φ(9):9 = 3²,1..9 中与 9 互质的是 1,2,4,5,7,8,共 6 个 ✓

💡 两个分支的正确性验证

分支一例:i = 4,p = 2。i % p == 0,所以 φ(8) = φ(4) × 2 = 2 × 2 = 4。手算:1..8 中与 8 互质的是 1,3,5,7,共 4 个 ✓

分支二例:i = 4,p = 3。4 % 3 ≠ 0,所以 φ(12) = φ(4) × (3 − 1) = 2 × 2 = 4 ✓(这正是积性:gcd(4,3) = 1)

案例案例2:P2158 [SDOI2008] 仪仗队 经典应用

题目:N × N 的方阵,教官站在左上角 (0,0),每个格子站一个人。问教官能看见多少个同学(不含自己)。数据范围:1 ≤ N ≤ 40000,时间限制 1s,空间限制 128MB。

🔍 分析过程

第1步:把几何问题翻译成数论 — 同学在 (x, y),若 gcd(x, y) = d > 1,则 (x/d, y/d) 处的人与他在同一条射线上且更近,把他挡住了。所以可见 ⟺ gcd(x, y) = 1。

第2步:拆成三部分 — 坐标范围 0 ≤ x, y ≤ N−1,排除原点。

· 坐标轴上的点:x = 0 时只有 (0, 1) 可见;y = 0 时只有 (1, 0) 可见,共 2 个。

· x, y ≥ 1 的互质有序对:记 M = N − 1。无序对(x ≤ y)中 gcd = 1 的个数恰好是 Σ_{i=1}^{M} φ(i) —— 因为对每个 y,满足 1 ≤ x ≤ y 且 gcd(x, y) = 1 的 x 有 φ(y) 个。有序对就是它的两倍,但要减去重复计入一次的 (1, 1),得 2Σφ(i) − 1。

第3步:合起来 — 总答案 = 2 + (2Σφ(i) − 1) = 2Σ_{i=1}^{N−1} φ(i) + 1。

第4步:边界 — N = 1 时只有教官自己,答案为 0。

第5步:复杂度 — 欧拉筛 O(N),N = 40000 时几乎瞬间完成。若用暴力双重循环判 gcd 则是 O(N² log N) ≈ 1.6×10⁹,必然超时。

🔦 N = 5 时的可见点(粉色为可见,灰色为被挡)
  1. 黄色点是教官自己,不计入答案。
  2. 粉色点满足 gcd(x, y) = 1,射线上没有更近的人,所以可见。
  3. 灰色点满足 gcd(x, y) > 1,例如 (2, 2) 被 (1, 1) 挡住,(3, 3) 被 (1, 1) 挡住,(4, 2) 被 (2, 1) 挡住。
  4. 统计:粉色点共 13 个,而公式 2 × (φ(1)+φ(2)+φ(3)+φ(4)) + 1 = 2 × (1+1+2+2) + 1 = 13 ✓
C++P2158 仪仗队 — 完整代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int MAXN = 40005;
int phi[MAXN], prime[MAXN], cnt = 0;
bool vis[MAXN];

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

    if (n == 1) { printf("0\n"); return 0; }   // 只有教官自己

    int m = n - 1;                           // 坐标最大值
    phi[1] = 1;
    for (int i = 2; i <= m; i++) {
        if (!vis[i]) { prime[++cnt] = i; phi[i] = i - 1; }
        for (int j = 1; j <= cnt && i * prime[j] <= m; j++) {
            int x = i * prime[j];
            vis[x] = true;
            if (i % prime[j] == 0) { phi[x] = phi[i] * prime[j]; break; }
            phi[x] = phi[i] * (prime[j] - 1);
        }
    }

    ll ans = 1;                                 // 公式中的常数 1
    for (int i = 1; i <= m; i++) ans += 2LL * phi[i];  // 加上 2 × Σφ(i)
    printf("%lld\n", ans);
    return 0;
}
运行结果
输入: 4
输出: 9
(验算:2 × (φ(1)+φ(2)+φ(3)) + 1 = 2 × (1+1+2) + 1 = 9 ✓ 与官方样例一致)

输入: 1
输出: 0
(只有教官一人,看不见任何同学)

输入: 5
输出: 13
(2 × (1+1+2+2) + 1 = 13,与上图统计一致)

输入: 40000
输出: 971004771
(N 最大时答案约 9.7×10^8,用 long long 存储;
  欧拉筛 O(N) 约 1 毫秒完成,暴力双重循环需要约 1.6×10^9 次 gcd,必然 TLE)

💡 从"格点"到"欧拉函数"的通用套路

凡是"从原点能看见多少个格点"的问题,本质都是在数互质的整数对,而互质对的计数几乎总是归结为 φ 的前缀和。

同类变形还有很多:"最简分数的个数"(分母 ≤ n 的真分数有 Σφ(i) 个)、"法里数列的长度"、"n × n 网格中斜率不同的直线条数" —— 都会落到同一个公式上。

案例案例3:扩展欧拉定理降幂(指数上万位) 终极武器

题目:给定正整数 a、m,以及一个极大整数 b(位数远超 long long 的表示范围,需按字符串读入),求 a^b mod m。注意 a 与 m 不一定互质。

🔍 分析过程

第1步:算 φ(m) — 用 O(√m) 的分解质因数法求出 φ(m)。

第2步:处理超大指数 — 把 b 当字符串读入。一边扫描一边维护 r = b mod φ(m),同时用一个布尔变量记录"b 是否曾经 ≥ φ(m)"。

第3步:判定与加 φ(m) — 若 b ≥ φ(m),则令 r += φ(m);若 b < φ(m),说明整个 b 都能塞进 long long,r 直接就是 b 本身,不加。

第4步:快速幂 — 计算 (a mod m)^r mod m。

第5步:注意 φ(m) = 1 的退化情况 — 当 m = 1 或 m = 2 时 φ(m) = 1,此时 r = b mod 1 + 1 = 1,仍然正确(因为任何数的 1 次方再取模不会改变结果)。

复杂度:O(√m + len(b) + log m),对 len(b) = 10⁷ 也只需一瞬间。

C++扩展欧拉定理降幂 — 完整代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

// 单点求 φ(m),O(√m)
ll phi(ll n) {
    ll res = n;
    for (ll p = 2; p * p <= n; p++)
        if (n % p == 0) {
            res = res / p * (p - 1);
            while (n % p == 0) n /= p;
        }
    if (n > 1) res = res / n * (n - 1);
    return res;
}

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

int main() {
    ll a, m;
    string b;
    cin >> a >> m >> b;

    ll ph = phi(m);

    // 边读边取模,并记录 b 是否 >= φ(m)
    ll r = 0;
    bool big = false;
    for (char c : b) {
        // 用 __int128 中转,防止 m 很大时 r*10 溢出
        __int128 t = (__int128)r * 10 + (c - '0');
        if (t >= ph) { big = true; t %= ph; }
        r = (ll)t;
    }

    // 扩展欧拉定理:b >= φ(m) 时补上一个 φ(m)
    if (big) r += ph;

    cout << qpow(a, r, m) << endl;
    return 0;
}
运行结果
输入: 2 12 5
输出: 8
(gcd(2,12)=2 不互质,必须用扩展形式。
  φ(12)=4,b=5 ≥ 4 → r = 5 mod 4 + 4 = 5
  2^5 = 32,32 mod 12 = 8 ✓
  若错误地只取 5 mod 4 = 1,会得到 2^1 = 2 ✗)

输入: 3 12 1000
输出: 9
(φ(12)=4,1000 ≥ 4 → r = 0 + 4 = 4,3^4 = 81 ≡ 9 ✓)

输入: 2 7 1000000
输出: 2
(gcd(2,7)=1,φ(7)=6,1000000 mod 6 = 4,b ≥ 6 → r = 4 + 6 = 10
  2^10 = 1024 ≡ 2 (mod 7),与 2^4 = 16 ≡ 2 一致 ✓)

输入: 7 1 99999999999999999999999999
输出: 0
(m = 1 时任何数模 1 都是 0;φ(1)=1,r = 0 + 1 = 1,快速幂返回 1 % 1 = 0 ✓)

💡 与普通降幂的三点区别

区别一:普通欧拉降幂要求 gcd(a, m) = 1,扩展形式不需要。这是最本质的差别。

区别二:扩展形式必须先判断 b ≥ φ(m)。如果 b 本身很小,加 φ(m) 会让指数变大,虽然幂值在循环段内仍相等,但前提是 b 已经进入循环段 —— 所以判据不能省。

区别三:代码里用 __int128 中转乘法,因为 m 可能达到 10⁹ 甚至 10¹⁸,两个接近 m 的数相乘会超出 long long。同理,边读边取模那一步的 r * 10 也要用 __int128。

模块 03

实践应用

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

题1单点求欧拉函数(两种方法对比)
题1模板题方法对比

求 φ(n)

题目背景:求单个数的欧拉函数值是数论题的高频操作,有"分解质因数"和"暴力枚举 gcd"两条路。

输入格式:一行一个整数 n(1 ≤ n ≤ 10¹²)。

输出格式:一行一个整数 φ(n)。

输入样例:12

输出样例:4

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

方法一:暴力枚举 + gcd 仅适合小 n

对 k 从 1 到 n 逐个判断 gcd(k, n) 是否为 1,复杂度 O(n log n)。n = 10⁶ 时约 10⁶ 次 gcd 调用,能过;但 n = 10¹² 时完全不可行。

方法二:分解质因数 + 计算式 正解

用 φ(n) = n × ∏(1 − 1/pᵢ),只需试除到 √n。n = 10¹² 时约 10⁶ 次除法,瞬间完成。

关键注意点:式子里只出现不同的质因子,所以每个质因子要把它的幂除干净(while (n % p == 0) n /= p;),然后才继续试下一个 p。

C++单点求 φ(n) — 完整代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

ll phi(ll n) {
    ll res = n;
    for (ll p = 2; p * p <= n; p++) {
        if (n % p == 0) {
            res = res / p * (p - 1);        // 先除后乘,防溢出
            while (n % p == 0) n /= p;     // 除干净,保证只用不同质因子
        }
    }
    if (n > 1) res = res / n * (n - 1);  // 剩下的大质因子
    return res;
}

int main() {
    ll n; cin >> n;
    cout << phi(n) << endl;
    return 0;
}
运行结果
输入: 12
输出: 4
(12 = 2²×3 → 12 × (1-1/2) × (1-1/3) = 12 × 1/2 × 2/3 = 4 ✓)

输入: 1000000007
输出: 1000000006
(这是素数,φ(p) = p - 1 ✓)

输入: 1000000000000
输出: 400000000000
(10^12 = 2^12 × 5^12 → 10^12 × 1/2 × 4/5 = 4×10^11 ✓
  若用暴力枚举需要 10^12 次循环,这题必须用分解质因数法)

输入: 1
输出: 1
(约定值)

💡 为什么"先除后乘"一定正确?

当前 res 的含义是"n 在剔除已处理的质因子时,按比例缩小后的值"。当发现新质因子 p 时,res 必然含有因子 p —— 因为 res 的初始值 n 含 p,而之前只除过其它质因子的幂。

所以 res / p 一定整除,先除再乘 (p − 1) 既不损失精度也不溢出。反之若先乘 (p−1) 再除 p,中间结果可能达到 res × p ≈ 10¹² × 10⁶ = 10¹⁸,虽然勉强在 long long 内但不安全。

题2φ 的前缀和
题2筛法应用

互质对计数

题目背景:统计 1 ≤ x ≤ y ≤ n 中满足 gcd(x, y) = 1 的整数对 (x, y) 的个数 —— 这正是 φ 的前缀和。

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

输出格式:一行一个整数,表示无序互质对(x ≤ y)的个数。

输入样例:6

输出样例:12

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

🔍 分析过程

第1步:按 y 分组 — 固定 y,统计满足 1 ≤ x ≤ y 且 gcd(x, y) = 1 的 x 的个数。这正是 φ(y) 的定义(注意 φ(y) 统计的是 1 ≤ x ≤ y,而 x = y 时 gcd(x,y) = y,只有 y = 1 才互质,恰好 φ(1) = 1 覆盖了这种情况)。

第2步:求和 — 答案是 Σ_{i=1}^{n} φ(i)。

第3步:求法选择 — 要整张表,用欧拉筛 O(n)。n = 10⁷ 时约 0.3 秒,注意数组要开 10⁷+5。

第4步:数据类型 — 求和结果约为 3n²/π²,n = 10⁷ 时约 6×10¹³,需要 long long。

验证 n = 6:φ 表为 1,1,2,2,4,2,和为 12。手工枚举:(1,1),(1,2),(1,3),(1,4),(1,5),(1,6),(2,3),(2,5),(3,4),(3,5),(4,5),(5,6) —— 共 12 对 ✓

C++φ 前缀和 — 完整代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int MAXN = 10000005;
int phi[MAXN], prime[MAXN], cnt = 0;
bool vis[MAXN];

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

    phi[1] = 1;
    for (int i = 2; i <= n; i++) {
        if (!vis[i]) { prime[++cnt] = i; phi[i] = i - 1; }
        for (int j = 1; j <= cnt && (ll)i * prime[j] <= n; j++) {
            int x = i * prime[j];
            vis[x] = true;
            if (i % prime[j] == 0) { phi[x] = phi[i] * prime[j]; break; }
            phi[x] = phi[i] * (prime[j] - 1);
        }
    }

    ll sum = 0;
    for (int i = 1; i <= n; i++) sum += phi[i];
    printf("%lld\n", sum);
    return 0;
}
运行结果
输入: 6
输出: 12
(φ(1..6) = 1,1,2,2,4,2,和为 12 ✓)

输入: 10
输出: 32
(φ = 1,1,2,2,4,2,6,4,6,4,和为 32 ✓)

输入: 10000000
输出: 30396356401022
(n = 10^7 时约 3.04×10^13,必须用 long long;
  O(n) 欧拉筛约 0.3 秒完成)

💡 一个漂亮的恒等式

有序互质对(x, y 都取 1..n)的个数 = 2Σφ(i) − 1。减 1 是因为 (1,1) 既算在 x ≤ y 里也算在 x ≥ y 里,被重复计了一次。

把这个数再除以 n²,当 n → ∞ 时极限是 6/π² ≈ 0.6079 —— 也就是说,随机取两个正整数,它们互质的概率约为 60.79%。这个结论和欧拉解决巴塞尔问题(Σ1/n² = π²/6)密切相关。

题3非互质情形下的降幂
题3综合应用

底数与模数不互质的大指数幂

题目背景:底数与模数不互质时,普通欧拉降幂失效,必须使用扩展欧拉定理。

输入格式:第一行两个整数 a, m(a 可为 m 的倍数);第二行一个十进制大整数 b(长度 ≤ 10⁵)。

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

输入样例:2 12
5

输出样例:8

数据范围:1 ≤ a ≤ 10⁹,1 ≤ m ≤ 10⁹,b 的长度 ≤ 10⁵。时间限制 1s,空间限制 256MB。

方法一:十进制快速幂(不降幂) 安全但慢一点

把指数按十进制逐位处理:res ← res¹⁰ × a^(当前数字)。不需要任何定理,绝对正确,复杂度是 len 次快速幂调用。

方法二:扩展欧拉定理降幂 更快

一次 O(√m) 求 φ(m),再一趟 O(len) 扫描压缩指数,最后做一次快速幂。总复杂度 O(√m + len + log m),常数比方法一小得多。

正确性关键:必须判断 b ≥ φ(m) 再决定是否 + φ(m)。本题样例中 a = 2、m = 12,gcd = 2 不互质,正是扩展定理的典型场景。

C++非互质降幂 — 完整代码(含错误写法对照)
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

ll phi(ll n) {
    ll res = n;
    for (ll p = 2; p * p <= n; p++)
        if (n % p == 0) { res = res / p * (p - 1); while (n % p == 0) n /= p; }
    if (n > 1) res = res / n * (n - 1);
    return res;
}

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

int main() {
    ll a, m; string b;
    cin >> a >> m >> b;
    ll ph = phi(m);

    ll r = 0; bool big = false;
    for (char c : b) {
        __int128 t = (__int128)r * 10 + (c - '0');
        if (t >= ph) { big = true; t %= ph; }
        r = (ll)t;
    }
    if (big) r += ph;              // 扩展欧拉定理的核心一步

    cout << qpow(a, r, m) << endl;

    // 对照:不加 φ(m) 的错误答案(仅用于演示)
    ll rBad = 0;
    for (char c : b) rBad = (ll)(((__int128)rBad * 10 + (c - '0')) % ph);
    cerr << "若忘记加 φ(m) 会得到:" << qpow(a, rBad, m) << endl;
    return 0;
}
运行结果
输入: 2 12
      5
输出: 8
(stderr 输出:若忘记加 φ(m) 会得到:2
  φ(12)=4,b=5 ≥ 4 → r = 1 + 4 = 5,2^5 = 32 ≡ 8 ✓
  错误写法 r = 1 得 2^1 = 2 ✗)

输入: 3 12
      1000
输出: 9
(φ(12)=4,1000 mod 4 = 0,b ≥ 4 → r = 0 + 4 = 4,3^4 = 81 ≡ 9 ✓
  错误写法 r = 0 得 3^0 = 1 ✗)

输入: 8 8
      100
输出: 0
(φ(8)=4,100 mod 4 = 0 → r = 4,8^4 是 8 的倍数 → 0 ✓)

输入: 5 999999937
      99999999999999999999999999999999
输出: 450112773
(a 与 m 互质,此时扩展形式与普通欧拉降幂结果相同,
  说明扩展定理兼容了互质的特殊情况)

💡 为什么会"忘记加"就错?

因为 a^k mod m 作为 k 的函数,并不是从 k = 0 开始就有周期的。以 2^n mod 12 为例:n = 0 得 1,n = 1 得 2,n = 2 得 4,n = 3 得 8,n = 4 得 4,n = 5 得 8 …… 从 n = 3 起才进入"4, 8, 4, 8"的循环。

如果只取 b mod φ(m),很可能把指数落到循环之前的"暂态"部分(比如 b = 1000 时算出 r = 0,落到 n = 0 的暂态),结果自然错了。加上 φ(m) 后指数一定 ≥ φ(m),稳稳落在循环段内部。

模块 04

总结提升

①核心要点与万能模板
知识板块必须记住的核心复杂度
欧拉函数定义1..n 中与 n 互质的个数,φ(1) = 1—
素数情形φ(p) = p − 1—
素数幂情形φ(p^k) = p^k − p^(k−1) = p^(k−1)(p−1)—
积性gcd(a,b)=1 → φ(ab) = φ(a)φ(b)—
计算式φ(n) = n × ∏(1 − 1/pᵢ),只用不同质因子O(√n)
约数求和Σ_{d|n} φ(d) = n—
欧拉筛求表整除分支 ×p,否则 ×(p−1)O(n)
欧拉定理gcd(a,m)=1 → a^φ(m) ≡ 1—
扩展欧拉定理b ≥ φ(m) → a^b ≡ a^(b mod φ(m) + φ(m))O(√m + len)
与费马的关系m 为素数时 φ(m) = m − 1,退化为费马小定理—
典型应用可见点 = 2Σφ(i)+1、Σgcd、RSA 加密—
C++万能模板:φ 两种求法 + 扩展欧拉定理降幂
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

/* ===== 1. 单点求 φ:O(√n) 分解质因数 ===== */
ll phi(ll n) {
    ll res = n;
    for (ll p = 2; p * p <= n; p++)
        if (n % p == 0) {
            res = res / p * (p - 1);
            while (n % p == 0) n /= p;
        }
    if (n > 1) res = res / n * (n - 1);
    return res;
}

/* ===== 2. 欧拉筛求整张 φ 表:O(n) ===== */
const int MAXN = 1000005;
int phiTab[MAXN], prime[MAXN], pcnt;
bool vis[MAXN];
void buildPhi(int n) {
    phiTab[1] = 1;
    for (int i = 2; i <= n; i++) {
        if (!vis[i]) { prime[++pcnt] = i; phiTab[i] = i - 1; }
        for (int j = 1; j <= pcnt && (ll)i * prime[j] <= n; j++) {
            int x = i * prime[j]; vis[x] = true;
            if (i % prime[j] == 0) { phiTab[x] = phiTab[i] * prime[j]; break; }
            phiTab[x] = phiTab[i] * (prime[j] - 1);
        }
    }
}

/* ===== 3. 快速幂(__int128 防溢出) ===== */
ll qpow(ll base, ll exp, ll mod) {
    ll res = 1 % mod; base %= mod;
    while (exp) {
        if (exp & 1) res = (__int128)res * base % mod;
        base = (__int128)base * base % mod;
        exp >>= 1;
    }
    return res;
}

/* ===== 4. 扩展欧拉定理降幂(a 与 m 可不互质) ===== */
ll powModBigExp(ll a, const string &b, ll m) {
    ll ph = phi(m), r = 0; bool big = false;
    for (char c : b) {
        __int128 t = (__int128)r * 10 + (c - '0');
        if (t >= ph) { big = true; t %= ph; }
        r = (ll)t;
    }
    if (big) r += ph;
    return qpow(a, r, m);
}

int main() {
    cout << phi(12) << endl;                            // 4
    buildPhi(12);
    cout << phiTab[9] << " " << phiTab[11] << endl;         // 6 10
    cout << powModBigExp(2, "5", 12) << endl;                // 8
    return 0;
}
🔑 考场三步走

① 要几个 φ?一个 → 分解质因数 O(√n);一批 → 欧拉筛 O(n)。
② 要降幂吗?先看 gcd(a, m) 是否为 1:是 → 普通欧拉定理;不是 → 扩展形式,别忘了 +φ(m)。
③ 指数怎么读?超过 10¹⁸ 一律用字符串边读边取模。

②拓展案例
拓展1:Σ gcd(i, n) —— 用 φ 求 gcd 之和

问题:给定 n(n ≤ 10⁷),求 Σ_{i=1}^{n} gcd(i, n)。

思路 按 gcd 分组

设 gcd(i, n) = d(d 必是 n 的约数)。则 gcd(i/d, n/d) = 1,且 1 ≤ i/d ≤ n/d。满足条件的 i/d 的个数恰好是 φ(n/d)。

所以 Σ_{i=1}^{n} gcd(i, n) = Σ_{d|n} d · φ(n/d)。

把 n 分解质因数后枚举所有约数 d,再逐个求 φ(n/d),复杂度约 O(√n × √n),比暴力 O(n log n) 快得多。

C++Σ gcd(i, n) — 完整代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

ll phi(ll n) {
    ll res = n;
    for (ll p = 2; p * p <= n; p++)
        if (n % p == 0) { res = res / p * (p - 1); while (n % p == 0) n /= p; }
    if (n > 1) res = res / n * (n - 1);
    return res;
}

int main() {
    ll n; cin >> n;
    ll ans = 0;
    for (ll d = 1; d * d <= n; d++) {
        if (n % d) continue;
        ans += d * phi(n / d);                     // 约数 d
        if (d != n / d) ans += (n / d) * phi(d);   // 配对约数 n/d
    }
    cout << ans << endl;
    return 0;
}
运行结果
输入: 7
输出: 13
(gcd(1..7, 7) = 1,1,1,1,1,1,7,和为 13 ✓
  公式:约数 d = 1, 7 → 1×φ(7) + 7×φ(1) = 6 + 7 = 13 ✓)

输入: 12
输出: 40
(手算:gcd(1..12, 12) = 1,2,3,4,1,6,1,4,3,2,1,12
  和 = 1+2+3+4+1+6+1+4+3+2+1+12 = 40 ✓
  公式:约数 d = 1, 2, 3, 4, 6, 12
  d×φ(12/d) = 1×φ(12) + 2×φ(6) + 3×φ(4) + 4×φ(3) + 6×φ(2) + 12×φ(1)
            = 1×4 + 2×2 + 3×2 + 4×2 + 6×1 + 12×1
            = 4 + 4 + 6 + 8 + 6 + 12 = 40 ✓)

输入: 1000000007
输出: 1000000008
(素数 n 时 Σgcd = 1×φ(n) + n×φ(1) = (n-1) + n = 2n - 1 ✓)

💡 两个小坑

坑一:φ 的小表要背熟。φ(1)=1、φ(2)=1、φ(3)=2、φ(4)=2、φ(5)=4、φ(6)=2、φ(8)=4、φ(12)=4。上面 d = 3 那一项是 3×φ(4) = 3×2 = 6,而不是 3×1 —— 一旦记错,公式明明用对了也照样算不出正确答案。

坑二:配对约数别漏。枚举 d 从 1 到 √n,当 d ≠ n/d 时必须把 n/d 也计入一次,否则会漏掉大于 √n 的那些约数(例如 n = 12 时的 4、6、12)。

拓展2:RSA 加密的数学原理(欧拉定理的工业级应用)

问题:如何在不安全信道上传递消息,让窃听者拿到密文也无法还原?

RSA 的构造流程 欧拉定理

① 选两个大素数 p 与 q(各 1024 位以上),令 n = pq。

② 计算 φ(n) = (p−1)(q−1),由积性得(因为 gcd(p, q) = 1)。

③ 选公钥指数 e:取一个与 φ(n) 互质的数(常用 65537)。

④ 求私钥指数 d:d = inv(e) mod φ(n),即 e·d ≡ 1 (mod φ(n))。

⑤ 加密:c = m^e mod n(m 是明文数值)。解密:m = c^d mod n。

为什么解密能还原?因为 c^d = m^(e·d) = m^(1 + k·φ(n)) = m × (m^φ(n))^k ≡ m × 1^k = m (mod n) —— 中间这一步正是欧拉定理(要求 gcd(m, n) = 1)。

为什么安全?只知道 n 无法反推 p、q,因而算不出 φ(n),也就求不出 d。大整数分解目前没有多项式算法。

C++RSA 玩具演示(小素数) — 完整代码
#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;
}

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

int main() {
    ll p = 61, q = 53;              // 演示用的小素数
    ll n = p * q;                        // 3233
    ll phiN = (p - 1) * (q - 1);        // 3120
    ll e = 17;                         // gcd(17, 3120) = 1

    ll x, y; exgcd(e, phiN, x, y);
    ll d = (x % phiN + phiN) % phiN;     // d = inv(e) mod φ(n) = 2753
    printf("n=%lld  phi(n)=%lld  公钥e=%lld  私钥d=%lld\n", n, phiN, e, d);

    ll msg = 65;                        // 明文(ASCII 的 'A')
    ll c = qpow(msg, e, n);              // 加密
    ll back = qpow(c, d, n);             // 解密
    printf("明文 %lld -> 密文 %lld -> 还原 %lld  %s\n", msg, c, back, back == msg ? "OK" : "FAIL");
    return 0;
}
运行结果
n=3233  phi(n)=3120  公钥e=17  私钥d=2753
明文 65 -> 密文 2790 -> 还原 65  OK

验算解密公式:
  c^d = 2790^2753 mod 3233
  由 e·d = 17 × 2753 = 46801 = 1 + 15 × 3120 = 1 + 15 × φ(n)
  所以 m^(e·d) = m^(1 + 15·φ(n)) = m × (m^φ(n))^15 ≡ m × 1 = m (mod n) ✓

注意:真实 RSA 的 p、q 都有 1024 位以上,
      n 约 617 位十进制,暴力分解完全不可行。
拓展3:可见点问题的二维推广

问题:求 Σ_{i=1}^{n} Σ_{j=1}^{n} [gcd(i, j) = 1](有序互质对个数),n ≤ 10⁷。

方法一:直接用 φ 前缀和 O(n)

由前面的分析,有序互质对个数 = 2 Σ_{i=1}^{n} φ(i) − 1。欧拉筛打表后 O(1) 得到答案。

方法二:容斥 / 莫比乌斯反演 能扩展到更大 n

记 F(n) = Σ_{i,j≤n} [gcd(i,j) = 1]。用容斥(先数"gcd 是 d 的倍数"再逐层减)可以写成
F(n) = Σ_{d=1}^{n} μ(d) · ⌊n/d⌋²,其中 μ 是莫比乌斯函数。

当 n 达到 10¹¹ 时方法一失效(数组开不下),但方法二可以配合整除分块做到 O(√n) —— 这就是后续学习莫比乌斯反演的入口。

C++有序互质对计数 — 完整代码
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int MAXN = 10000005;
int phi[MAXN], prime[MAXN], cnt;
bool vis[MAXN];

int main() {
    int n; scanf("%d", &n);
    phi[1] = 1;
    for (int i = 2; i <= n; i++) {
        if (!vis[i]) { prime[++cnt] = i; phi[i] = i - 1; }
        for (int j = 1; j <= cnt && (ll)i * prime[j] <= n; j++) {
            int x = i * prime[j]; vis[x] = true;
            if (i % prime[j] == 0) { phi[x] = phi[i] * prime[j]; break; }
            phi[x] = phi[i] * (prime[j] - 1);
        }
    }
    ll s = 0;
    for (int i = 1; i <= n; i++) s += phi[i];
    printf("%lld\n", 2 * s - 1);       // 有序互质对个数
    return 0;
}
运行结果
输入: 6
输出: 23
(Σφ(1..6) = 12,2×12 - 1 = 23 ✓
  无序对 12 个,加上对称的 11 个(去掉重复的 (1,1))得 23)

输入: 10
输出: 63
(Σφ(1..10) = 32,2×32 - 1 = 63 ✓ 与 C4 题的容斥结果一致)

输入: 10000000
输出: 60792708433043
(n = 10^7 时约 6.08×10^13,
  6.08×10^13 / 10^14 ≈ 0.6079,与理论极限 6/π² ≈ 0.60793 吻合 ✓)
③思考与启发

💡 三个值得深挖的问题

思考1:欧拉定理为什么必须要求 gcd(a, m) = 1?不互质的时候,"幂序列"到底长什么样?

查看参考答案

必要性:证明第 3 步要用到"a·rᵢ 仍与 m 互质"。若 gcd(a, m) = d > 1,那么 a·rᵢ 就被 d 整除,不再与 m 互质,"重排"的论证立刻崩塌。

具体反例:m = 8、a = 2。φ(8) = 4,但 2^4 = 16 ≡ 0 (mod 8) ≠ 1,定理不成立。

不互质时的结构:幂序列会先经过一段"暂态",之后进入循环。例如 2^n mod 8:1, 2, 4, 0, 0, 0, … 从 n = 3 起恒为 0;2^n mod 12:1, 2, 4, 8, 4, 8, …(从 n = 2 起循环,长度 2,是 φ(12) = 4 的因子)。

结论:不互质时定理失效,但"最终周期性"仍然成立,这正是扩展欧拉定理能补一个 φ(m) 把指数推进循环段的原因。

易错:看到"欧拉定理"四个字就直接套公式,不检查 gcd。

思考2:φ 是积性函数,为什么不是完全积性函数?两者的差别在代码实现上体现在哪里?

查看参考答案

定义差别:积性只要求"对互质的 a, b 有 f(ab) = f(a)f(b)";完全积性要求"对任意 a, b 都成立"。

φ 为什么不完全积性:φ(4) = 2,而 φ(2)·φ(2) = 1,因为 gcd(2,2) = 2 ≠ 1。本质原因是 φ(p^k) = p^k − p^(k−1) 与 (φ(p))^k = (p−1)^k 不相等。

代码里的体现:欧拉筛求 φ 时,i % prime[j] == 0 分支必须写 phi[i] * prime[j],而不是 phi[i] * (prime[j] - 1)。这个分支处理的正是"p 已是 i 的因子、gcd(i, p) ≠ 1"的情形 —— 用完全积性的写法就会错。

对比:μ(莫比乌斯函数)也是积性但非完全积性;而幂函数 f(n) = n^k 是完全积性的。

易错:把两个分支的公式记反,导致 φ 表从某个合数开始整体错乱。

思考3:扩展欧拉定理里,为什么必须判断 b ≥ φ(m) 才能加 φ(m)?如果 b < φ(m) 也照样加,会怎样?

查看参考答案

会算错。取 m = 8、a = 2、b = 2(此时 φ(8) = 4,2 < 4)。正确结果 2^2 = 4。

若强行加 φ(m):r = 2 + 4 = 6,2^6 = 64 ≡ 0 (mod 8) ≠ 4 ✗。

原因:2^n mod 8 的序列是 1, 2, 4, 0, 0, 0, …,n = 2 处在"暂态",n = 6 已经在稳态,两者值不同。加 φ(m) 相当于把指数往前推了一整段,可能跨越暂态与稳态的分界。

反例二:m = 12、a = 2、b = 1。正确结果 2^1 = 2。若加 φ(m) 得 r = 5,2^5 = 32 ≡ 8 ✗。

所以:b < φ(m) 时必须直接算,不能加。实现时用一个布尔变量记录"扫描过程中是否出现过 ≥ φ(m) 的时刻"即可。

易错:为了"保险"无条件 r += φ(m),在 b 很小的数据点上全部答错。

模块 05

学习检测

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

易错1

把 φ(p^k) 写成 (p−1)^k

❌ 错误
看到 8 = 2³ 就写 φ(8) = (2−1)³ = 1。正确值是 4(1, 3, 5, 7 四个数与 8 互质),差了 4 倍,后面的欧拉定理降幂全部失准。
✅ 正确
用公式 φ(p^k) = p^k − p^(k−1) = p^(k−1)(p−1)。φ(8) = 8 − 4 = 4;φ(9) = 9 − 3 = 6;φ(1024) = 1024 − 512 = 512。只有 k = 1 时 φ(p) = p − 1 才对。
口诀:幂次越高活得久,φ 不是简单乘方!
易错2

不检查 gcd(a, m) 就用欧拉定理

❌ 错误
m = 8、a = 2,直接写 2^φ(8) = 2^4 ≡ 1。实际 2^4 = 16 ≡ 0 (mod 8),整数都没对上。原因是 gcd(2, 8) = 2 ≠ 1,定理前提被破坏。
✅ 正确
用定理前先算一次 gcd(a, m)。等于 1 → 欧拉定理可用;大于 1 → 改用扩展欧拉定理(记得判 b ≥ φ(m)),或者把底数先对 m 取模后看是否还需特判。
口诀:欧拉定理先问 gcd,不互质就上扩展版!
易错3

扩展欧拉定理漏判 b ≥ φ(m)

❌ 错误
为了"保险"一律写成 e = b % φ(m) + φ(m)。取 m = 8、a = 2、b = 2(b = 2 < φ(8) = 4),得 e = 6,2^6 = 64 ≡ 0,而正确答案是 2² = 4。
✅ 正确
先判断:若 gcd(a, m) = 1,直接 e = b % φ(m);若不互质,则需 b ≥ φ(m) 才加 φ(m),否则 e = b 原样计算。指数用字符串读入时,边读边记一个布尔量 big。
口诀:小指数不许动,够大了才能加 φ!
易错4

线性筛求 φ 时两个分支的公式记反

❌ 错误
写成 if (i % p == 0) phi[i*p] = phi[i] * (p - 1);。这个写法只在 p 与 i 互质时成立;当 p | i 时 gcd(i, p) = p,φ 不再有积性,整张表从第一个合数起就开始错。
✅ 正确
分支一 p | i:phi[i*p] = phi[i] * p(多出一整倍);分支二 p ∤ i:phi[i*p] = phi[i] * (p - 1)(积性)。两个分支都必须出现,且顺序不能颠倒。
口诀:整除乘 p,不整除乘 p−1!
易错5

φ(1) 与 n = 1 的边界处理

❌ 错误
筛法里让 phi[1] = 0(因为"没有比 1 小且与 1 互质的数")。结果求 φ 前缀和或 Σ_{d|n} φ(d) 时,每一项都少 1,n = 1 的答案直接错。
✅ 正确
按定义 φ(1) = 1(gcd(1, 1) = 1,1 自己算一个)。筛法初始化 phi[1] = 1;单点分解法里循环结束后若 n > 1 再补一步,n = 1 时直接返回 1。
口诀:φ(1) 等于一,求和不会漏!
📋速查表
结论 / 公式内容前提条件
定义φ(n) = #{1 ≤ k ≤ n : gcd(k, n) = 1}φ(1) = 1
素数φ(p) = p − 1p 为素数
素数幂φ(p^k) = p^k − p^(k−1)p 为素数,k ≥ 1
积性φ(ab) = φ(a)·φ(b)gcd(a, b) = 1
计算式(容斥)φ(n) = n · ∏(1 − 1/p )p 取遍 n 的所有不同素因子
约数求和恒等式Σ_{d|n} φ(d) = nn ≥ 1
gcd 求和Σ_{i=1}^{n} gcd(i, n) = Σ_{d|n} d·φ(n/d)n ≥ 1
欧拉定理a^φ(m) ≡ 1 (mod m)gcd(a, m) = 1
扩展欧拉定理a^b ≡ a^(b mod φ(m) + φ(m)) (mod m)b ≥ φ(m)(否则不降幂)
与费马的关系m = p 时 φ(p) = p − 1,欧拉定理退化为费马小定理p 为素数
复杂度对照单点 φ:O(√n) / 筛 1..n:O(n) / 单次幂:O(log b)—
📌笔记卡
必记概念
  • φ(n) 是"与 n 互质的数"的个数
  • φ(p) = p−1;φ(p^k) = p^(k−1)(p−1)
  • φ 是积性函数,但不是完全积性
  • 欧拉定理:a^φ(m) ≡ 1(gcd = 1)
  • Σ_{d|n} φ(d) = n 是常用恒等式
必背模板
  • 单点 phi(n):O(√n) 边分解边乘 (p−1)/p
  • 线性筛 phi:两个分支 p | i 与 p ∤ i
  • phi 表初始化 phi[1] = 1
  • 扩展欧拉定理:读指数时记 big 标志
  • 降幂模板:e = big ? r + p : r
考场提醒
  • 先问"a 与 m 互质吗"
  • 不互质时必写扩展欧拉定理
  • 指数用字符串读,别用 long long
  • 中间乘法加 1LL * 或 __int128
  • φ(1) = 1,别初始化成 0
课后练习

分层练习作业

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

🟢 A组 · 基础(4题)

A1计算题

计算 φ(36)、φ(100)、φ(1024)、φ(1)。

查看参考答案

φ(36):36 = 2² × 3²,φ(36) = 36 × (1 − 1/2) × (1 − 1/3) = 36 × 1/2 × 2/3 = 12。

φ(100):100 = 2² × 5²,φ(100) = 100 × 1/2 × 4/5 = 40。

φ(1024):1024 = 2¹⁰,φ(2¹⁰) = 2¹⁰ − 2⁹ = 1024 − 512 = 512。

φ(1):按定义 gcd(1, 1) = 1,所以 1 自己算一个,φ(1) = 1。

验证:Σ_{d|36} φ(d) = φ(1)+φ(2)+φ(3)+φ(4)+φ(6)+φ(9)+φ(12)+φ(18)+φ(36) = 1+1+2+2+2+6+4+6+12 = 36 ✓

易错:把 φ(1024) 算成 (2−1)¹⁰ = 1;忘记 φ(1) = 1。

A2编程题

输入 n(n ≤ 10¹²),输出 φ(n)。要求 O(√n)。

查看参考答案

思路:把 n 做质因数分解,每找到一个素因子 p,就把答案乘上 (p−1)/p。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll phi(ll n) {
    if (n == 1) return 1;                 // φ(1) = 1,别漏
    ll r = n;
    for (ll i = 2; i * i <= n; i++)
        if (n % i == 0) {
            r = r / i * (i - 1);              // 先除后乘,避免溢出
            while (n % i == 0) n /= i;      // 把 i 除干净
        }
    if (n > 1) r = r / n * (n - 1);      // 剩下的大素因子
    return r;
}
int main() { ll n; cin >> n; cout << phi(n) << endl; return 0; }

样例:输入 36 → 12;输入 100 → 40;输入 1000000007 → 1000000006(该数是素数)。

易错:写成 r = r * (i - 1) / i,n 很大时先乘会溢出;循环条件写成 i <= n 导致死循环。

A3编程题

用线性筛求出 1 ~ n(n ≤ 10⁷)每个数的 φ,并输出其中最大值出现的位置。

查看参考答案

思路:在欧拉筛(线性筛素数)的框架上同步维护 phi 数组,两个分支分别处理。

#include <bits/stdc++.h>
using namespace std;
const int N = 10000010;
int phi[N], p[N], cnt;
bool vis[N];
int main() {
    int n; cin >> n;
    phi[1] = 1;                            // 关键初始化
    for (int i = 2; i <= n; i++) {
        if (!vis[i]) { p[++cnt] = i; phi[i] = i - 1; }   // i 是素数
        for (int j = 1; j <= cnt && i * p[j] <= n; j++) {
            int k = i * p[j];
            vis[k] = true;
            if (i % p[j] == 0) { phi[k] = phi[i] * p[j]; break; }  // 分支一:p | i
            else phi[k] = phi[i] * (p[j] - 1);                        // 分支二:积性
        }
    }
    int best = 1;
    for (int i = 2; i <= n; i++) if (phi[i] > phi[best]) best = i;
    cout << best << " " << phi[best] << endl;
    return 0;
}

样例:输入 10 → 输出 7 6(φ(7) = 6,φ(9) = 6 但 7 更小且更大值不存在;φ(10) = 4)。

易错:两个分支公式写反;忘记 phi[1] = 1;用 int 数组存 n = 10⁷ 时栈上开不下(要放全局或 static)。

A4证明题

证明恒等式:Σ_{d|n} φ(d) = n。

查看参考答案

证明(按 gcd 分类计数):考虑集合 S = {1, 2, 3, …, n},一共 n 个元素。

对每个 k ∈ S 定义 d = gcd(k, n)。显然 d 一定是 n 的约数,于是 S 被划分成若干组,每组对应一个约数 d:G_d = {k : 1 ≤ k ≤ n, gcd(k, n) = d}。

把 k = d·j 代入条件:gcd(d·j, n) = d ⟺ gcd(j, n/d) = 1,且 1 ≤ j ≤ n/d。所以 G_d 的元素个数恰好等于 φ(n/d)。

于是 n = |S| = Σ_{d|n} |G_d| = Σ_{d|n} φ(n/d)。当 d 遍历 n 的所有约数时,n/d 也在遍历所有约数,因此 n = Σ_{d|n} φ(d)。证毕。

验证:n = 12 时,左边 = φ(1)+φ(2)+φ(3)+φ(4)+φ(6)+φ(12) = 1+1+2+2+2+4 = 12 ✓

易错:忘记说明"d 一定是 n 的约数",导致分组不完整;把 j 的范围写成 1 ≤ j ≤ n,多算了倍数。

🟡 B组 · 提高(4题)

B1综合编程

输入 n(n ≤ 10⁶),输出 Σ_{i=1}^{n} φ(i) 与它在模 998244353 下的值。

查看参考答案

思路:先用线性筛打出 φ 表,再前缀累加。注意 1 ~ 10⁶ 的 φ 之和约 3×10¹¹,必须用 long long 存前缀和。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1000010, MOD = 998244353;
int phi[N], p[N], cnt;
bool vis[N];
int main() {
    int n; cin >> n;
    phi[1] = 1;
    for (int i = 2; i <= n; i++) {
        if (!vis[i]) { p[++cnt] = i; phi[i] = i - 1; }
        for (int j = 1; j <= cnt && i * p[j] <= n; j++) {
            int k = i * p[j]; vis[k] = true;
            if (i % p[j] == 0) { phi[k] = phi[i] * p[j]; break; }
            else phi[k] = phi[i] * (p[j] - 1);
        }
    }
    ll sum = 0, sumMod = 0;
    for (int i = 1; i <= n; i++) {
        sum += phi[i];
        sumMod = (sumMod + phi[i]) % MOD;
    }
    cout << sum << " " << sumMod << endl;
    return 0;
}

样例:输入 10 → Σ = 1+1+2+2+4+2+6+4+6+4 = 32,模值 32。

考点:φ 的线性筛 + 前缀和的数据类型选择。

易错:用 int 累加导致溢出(10⁶ 时约 3×10¹¹);取模时对中间变量用了 int 却先加后取。

B2综合编程

【扩展欧拉定理】输入 a、m 与十进制指数 b(位数可达 2×10⁷),输出 a^b mod m。

查看参考答案

思路:先求 φ(m),再把超长指数边读边对 φ(m) 取模,同时用 big 标志记录"是否出现过 ≥ φ(m) 的时刻"。最后 e = big ? r + φ(m) : r。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll a, m;
bool big;
ll phi(ll n) {
    if (n == 1) return 1;
    ll r = n;
    for (ll i = 2; i * i <= n; i++)
        if (n % i == 0) { r = r / i * (i - 1); while (n % i == 0) n /= i; }
    if (n > 1) r = r / n * (n - 1);
    return r;
}
ll qpow(ll b, ll e, ll p) {
    ll r = 1 % p; b %= p;
    while (e) {
        if (e & 1) r = (__int128)r * b % p;      // 用 __int128 中转防溢出
        b = (__int128)b * b % p; e >>= 1;
    }
    return r;
}
ll readExp(ll mod) {
    string s; cin >> s;
    ll r = 0;
    for (char c : s) {
        r = r * 10 + (c - '0');
        if (r >= mod) { big = true; r %= mod; }     // 一旦够大就标记
    }
    return r;
}
int main() {
    cin >> a >> m;
    ll ph = phi(m);
    ll e = readExp(ph);
    if (big) e += ph;                               // 扩展欧拉定理的关键一步
    cout << qpow(a, e, m) << endl;
    return 0;
}

样例:输入 2 / 7 / 4 → 2(2⁴ = 16 ≡ 2);输入 2 / 13 / 1000000000000000000 → 3(10¹⁸ mod 12 = 4,2⁴⁺¹² = 2¹⁶ ≡ 3)。

易错:对 φ(m) 取模却用 int 接收 r(m ≤ 10⁹ 时 φ(m) < 10⁹,尚可,但乘 10 会越界);忘记 big 判断,小指数被错误加 φ(m);乘法未用 __int128 中转。

B3分析题

有人说:"欧拉定理里的指数周期就是 φ(m),所以 a^k mod m 的周期一定是 φ(m)。"这句话对吗?以 m = 12、a = 5 为例说明。

查看参考答案

结论:不对。欧拉定理只说 a^φ(m) ≡ 1,即 φ(m) 是一个"归零周期";真正的最小正周期(元素的阶)是 φ(m) 的约数,可以严格更小。

例:m = 12、a = 5。φ(12) = 4,欧拉定理给出 5⁴ ≡ 1 (mod 12)。但 5¹ = 5,5² = 25 ≡ 1,真正的周期只有 2。

再看一例:m = 12、a = 7。φ(12) = 4,7² = 49 ≡ 1,周期同样是 2;而 a = 1 时周期就是 1。

正确的表述:若 gcd(a, m) = 1,则 a 关于模 m 的阶 k 一定满足 k | φ(m)(可由带余除法证明:设 φ(m) = qk + r,两边取幂得 a^r ≡ 1,由 r < k 与最小性推出 r = 0)。

实践意义:降幂时用 φ(m) 一定正确(因为 φ(m) 是周期的整数倍),但用更小的阶会让中间数字更小、更快。P5091 那类题目里通常直接用 φ(m) 就够了。

易错:把"存在一个周期"与"最小周期"混为一谈,或者用 φ(m) 去论证"周期不可能是别的值"。

B4综合编程

输入 n(n ≤ 10⁶),输出 Σ_{i=1}^{n} Σ_{j=1}^{n} gcd(i, j) 对 998244353 取模的结果。

查看参考答案

思路:按 gcd 的值分类。设 S(k) = #{1 ≤ i, j ≤ k : gcd(i, j) = 1} = 2·Φ(k) − 1(其中 Φ 是 φ 的前缀和,减去 1 是去掉 (1,1) 被算两次)。

那么 Σ_{i=1}^{n} Σ_{j=1}^{n} gcd(i, j) = Σ_{d=1}^{n} d · S(⌊n/d⌋),这里把 gcd 为 d 的数对全部缩放 d 倍变成互质对。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 1000010, MOD = 998244353;
int phi[N], p[N], cnt;
bool vis[N];
ll pre[N];                                   // φ 的前缀和
int main() {
    int n; cin >> n;
    phi[1] = 1;
    for (int i = 2; i <= n; i++) {
        if (!vis[i]) { p[++cnt] = i; phi[i] = i - 1; }
        for (int j = 1; j <= cnt && i * p[j] <= n; j++) {
            int k = i * p[j]; vis[k] = true;
            if (i % p[j] == 0) { phi[k] = phi[i] * p[j]; break; }
            else phi[k] = phi[i] * (p[j] - 1);
        }
    }
    for (int i = 1; i <= n; i++) pre[i] = pre[i - 1] + phi[i];
    ll ans = 0;
    for (int d = 1; d <= n; d++) {
        ll k = n / d;
        ll S = (2 * pre[k] - 1) % MOD;      // 互质对数
        ans = (ans + (ll)d % MOD * (S % MOD)) % MOD;
    }
    cout << ans << endl;
    return 0;
}

样例:输入 3 → 12(手算 gcd 矩阵:1+1+1+1+2+1+1+1+3 = 12 ✓)。

考点:φ 前缀和 + 整除分块思想(本题 O(n) 已够,若 n 到 10¹⁰ 则需数论分块)。

易错:忘记减 1(把 (1,1) 算了两次);2 * pre[k] 用 int 乘导致溢出;S 为负数时未加 MOD 再取模。

🔴 C组 · 挑战(4题)

C1初赛选择题

下列说法正确的有几个?
① 若 gcd(a, m) = 1,则 a^φ(m) ≡ 1 (mod m)
② φ(2^n) = 2^(n−1)
③ 若 a^k ≡ 1 (mod m) 的最小正周期为 k,则 k 一定整除 φ(m)(前提 gcd(a, m) = 1)
④ φ 是完全积性函数,即 φ(ab) = φ(a)φ(b) 对任意 a、b 成立

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

查看参考答案

答案:C(3 个)

①正确:这正是欧拉定理本身。

②正确:2^n 只有唯一素因子 2,φ(2^n) = 2^n − 2^(n−1) = 2^(n−1)。例如 φ(8) = 4。

③正确:阶整除 φ(m),用带余除法:设 φ(m) = qk + r (0 ≤ r < k),则 a^r ≡ 1,由 k 的最小性得 r = 0。

④错误:φ 只是积性(要求 gcd(a, b) = 1)。反例 a = b = 2:φ(4) = 2,而 φ(2)·φ(2) = 1,不相等。

考点:欧拉定理、φ(2^n)、阶的性质、积性与完全积性的区别。

易错:把"积性"与"完全积性"当同义词,误选 D。

C2构造与证明

用"重排"的思想证明欧拉定理:设 gcd(a, m) = 1,r₁, r₂, …, r_{φ(m)} 是模 m 的既约剩余系,证明 a^φ(m) ≡ 1 (mod m)。并指出证明中"gcd(a, m) = 1"这一步用在哪里。

查看参考答案

第一步(构造新集合):令 S = {a·r₁, a·r₂, …, a·r_{φ(m)}},这里每个数都按模 m 理解。

第二步(证明 S 与既约剩余系重排):对任意 i,由 gcd(a, m) = 1 且 gcd(rᵢ, m) = 1 得 gcd(a·rᵢ, m) = 1,所以 S 中每个元素都与 m 互质 —— 这一步正是 gcd(a,m)=1 的用处,注释①。

再证互不相同:若 a·rᵢ ≡ a·rⱼ (mod m),两边同乘 a 在模 m 下的逆元 a⁻¹(存在性同样依赖 gcd(a,m)=1,注释②),得 rᵢ ≡ rⱼ,故 i = j。

于是 S 是 φ(m) 个互不相同、都与 m 互质的模 m 剩余 —— 恰好与 {r₁, …, r_{φ(m)}} 构成同一个集合(重排)。

第三步(整体相乘):∏ᵢ (a·rᵢ) ≡ ∏ᵢ rᵢ (mod m),即 a^{φ(m)} · ∏ rᵢ ≡ ∏ rᵢ (mod m)。

因为每个 rᵢ 都与 m 互质,乘积 P = ∏ rᵢ 也与 m 互质,故 P 在模 m 下可逆。注释③:两边同乘 P⁻¹ 得 a^{φ(m)} ≡ 1 (mod m)。证毕。

三处依赖 gcd(a, m) = 1:① 保证 a·rᵢ 仍与 m 互质(重排成立);② 保证 a 可逆可约(单射成立);③ 保证 P 可逆(最后约去)。

易错:只说"重排"却不证明互不相同;或者漏掉最后的"约去 ∏rᵢ",直接宣布结论。

C3综合编程

【Longge 的问题】输入 n(n ≤ 2³²),求 Σ_{i=1}^{n} gcd(i, n)。

查看参考答案

思路:按 gcd 的值分类。gcd(i, n) = d 要求 d | n,且 i = d·j 满足 gcd(j, n/d) = 1 且 1 ≤ j ≤ n/d,这样的 j 恰有 φ(n/d) 个。所以 Σ_{i=1}^{n} gcd(i, n) = Σ_{d|n} d · φ(n/d)。

只需枚举 n 的约数(O(√n) 个),每个约数用 O(√(n/d)) 求 φ,总复杂度远低于暴力 O(n)。

#include <bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
ull phi(ull n) {
    if (n == 1) return 1;
    ull r = n;
    for (ull i = 2; i * i <= n; i++)
        if (n % i == 0) { r = r / i * (i - 1); while (n % i == 0) n /= i; }
    if (n > 1) r = r / n * (n - 1);
    return r;
}
int main() {
    ull n; cin >> n;
    ull ans = 0;
    for (ull d = 1; d * d <= n; d++) {
        if (n % d) continue;
        ull e = n / d;
        ans += d * phi(e);                       // 约数 d 配 n/d
        if (d != e) ans += e * phi(d);           // 配对的另一个约数
    }
    cout << ans << endl;
    return 0;
}

样例:输入 6 → 15(gcd(1..6, 6) = 1,2,3,2,1,6,和 = 15 ✓);输入 12 → 40。

考点:gcd 按值分类 + φ 的应用 + 约数对枚举。

易错:约数对只加了一半(漏掉 d != e 分支);用 int 存 n 导致 n = 2³² 时溢出(要用 long long/unsigned long long);d * d <= n 在 ull 下安全但 i * i <= n 对大 n 也要注意。

C4算法设计

【上帝与集合的正确用法】定义无穷幂塔 T = 2^(2^(2^(…)))(无限层)。给定 p(p ≤ 10⁷),求 T mod p。要求说明为什么可以直接套用扩展欧拉定理而不用判断指数大小。

查看参考答案

关键观察:无穷幂塔的"指数部分"本身就是一个无穷幂塔,它一定 ≥ φ(p)(幂塔增长极快,即使 p 很大,前几层就超过了)。所以扩展欧拉定理里 b ≥ φ(m) 的条件总能满足,可以无条件加 φ(m)。

递归定义:设 f(m) = T mod m。由扩展欧拉定理,f(m) = 2^(f(φ(m)) + φ(m)) mod m;边界 f(1) = 0(任何数模 1 都是 0)。

复杂度:φ(φ(…)) 每次至少减半,降到 1 只需 O(log p) 层,每层一次快速幂 O(log p),总 O(log²p)。

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N = 10000010;
int phi[N];                                     // 预处理到 10^7 的 φ 表
void sieve(int n) {
    for (int i = 1; i <= n; i++) phi[i] = i;
    for (int i = 2; i <= n; i++)
        if (phi[i] == i)                          // i 是素数
            for (int j = i; j <= n; j += i) phi[j] = phi[j] / i * (i - 1);
}
ll qpow(ll b, ll e, ll p) {
    ll r = 1 % p; b %= p;
    while (e) { if (e & 1) r = (__int128)r * b % p; b = (__int128)b * b % p; e >>= 1; }
    return r;
}
ll f(ll m) {
    if (m == 1) return 0;                       // 边界
    return qpow(2, f(phi[m]) + phi[m], m);      // 无条件加 φ(m)
}
int main() {
    sieve(10000000);
    int T; cin >> T;
    while (T--) { ll p; cin >> p; cout << f(p) << endl; }
    return 0;
}

样例:输入 3 → 输出 1(2 的偶次幂模 3 恒为 1);输入 10 → 输出 6(指数 ≡ 0 (mod 4),2⁴ = 16 ≡ 6)。

易错:递归里写了 big 判断反而不加 φ(m)(这里必须加);边界写成 m == 0;φ 表只筛到输入最大值的一半,导致 phi[m] 越界或为 0。

🏆 洛谷原题精选(12道)

以下 12 道题围绕"欧拉函数 / 线性筛 / 约数求和 / 欧拉定理降幂"展开,按难度从普及- 到提高排列。

1
P1403
[AHOI2005] 约数研究普及-
2
P1865
A % B Problem普及-
3
P3383
【模板】线性筛素数普及-
4
P2158
[SDOI2008] 仪仗队提高
5
P1447
[NOI2010] 能量采集提高
6
P1390
公约数的和提高
7
P2398
GCD SUM提高
8
P2568
GCD提高
9
P2303
[SDOI2012] Longge 的问题提高
10
P5091
【模板】扩展欧拉定理提高
11
P4139
上帝与集合的正确用法提高
12
P5221
Product提高

提示:第 10 ~ 12 题是欧拉定理的进阶应用 —— P5091 练模板,P4139 练递归降幂,P5221 需要先把乘式转成指数和再配合数论分块,建议先完成前两题再挑战第三题。