【算法笔记】Euler-Fermat 定理

  • 本文总计约 13000 字,阅读大约需要 40 分钟
  • 警告!警告!警告!本文有大量的 LATEX 公式渲染,可能会导致加载异常缓慢!

前言

如果一个人不学习 Euler 定理的话,我是绝对不会说他学过数论的。

作为数论中最基础的知识点,Euler 定理与 Fermat 小定理,Wilson 定理和中国剩余定理(CRT)并成为数论四大基本定理,它的重要性可见一斑。

后来又有数学家,将 Euler 定理做了一个拓展,得到了现在被我们广泛使用的定理— —扩展 Euler 定理,这也就是我们今天所要讲的知识。

扩展 Euler 定理可以把传统的快速幂加速得更快,同时,在研究群论的时候,Euler 定理也扮演着非常重要的角色。

所以,学习扩展 Euler 定理并不只是为了做 OI 题目的,它更是一块让我们研究更高层次的知识的垫脚石。

问题引入

给定正整数 a,b,m,求 abmodm 的值。
Subtask 11b1091a<m107
Subtask 21b101000000001a<m=998244353
Subtask 31b101000000001a<m109

从 Fermat 小定理开始

对于上面的 Subtask 1,我们可以直接写一个快速幂。时间复杂度为 Θ(logb)

代码实现起来很简单:

#include <cstdio>
using namespace std;
typedef long long ll;

ll a, b, m;

ll qpow(ll a, ll b, ll m) {  //快速幂模板
	ll ans = 1 % m;
	while(b) {
		if(b & 1) ans = (ans * a) % m;
		a = (a * a) % m; b >>= 1;
	}
	return ans;
}

int main(void) {
	scanf("%lld%lld%lld", &a, &b, &m);
	printf("%lld", qpow(a, b, m));
	return 0;
}
//by CaO

既然快速幂时间复杂度当然是 Θ(logb),当 b109 时,还是可以轻松水过的。

我们来看 Subtask 2

Subtask 2 中,b 的值达到了 10108,这就意味着,即使是用 Θ(logb) 的快速幂,在这个时候也要执行 108 步以上而且还要打烦人的高精度 QwQ,所以是妥妥的 TLE。

但是我们又注意到,m=998244353,它是一个质数。提起质数,我们应该想到什么?

当然是 Fermat 小定理

Fermat 小定理:若 p 是质数且 ap,则 ap11(modp)

我们再对原算式 abmodp 做个变型:

b(p1) 做带余除法,得到 b=k(p1)+r (r<p1),那么我们就有:

abak(p1)+r(ap1)karar(modp).

这也就是说,无论 b 有多大,只要 am 互质,我们都可以将 b 边读入,边取模,最后进行计算的时候,b 的值一定不会超过 m=998244353,也就是说,在这种情况下,我们把原本的 Θ(logb) 的时间复杂度优化成了 Θ(logm),当 m=998244353 的时候,无论 b 有多大,都可以轻松水过 QwQ。

代码只要基于原来的快速幂做一些小小的改动就可以:

#include <cstdio>
using namespace std;
typedef long long ll;
const int MOD = 998244353;  //Subtask 2 保证模数为 998244353

ll a, b, m;
char ch;

ll read(void) {  //快读,可以边读入边取模 
    ll w = 0; char ch = getchar();
    
    while(ch < '0' || ch > '9') ch = getchar();
    while(ch >= '0' && ch <= '9') {
        w = (w * 10 + ch - '0') % (MOD - 1); ch = getchar();
    }
    return w;
}

ll qpow(ll a, ll b, ll m) {
    ll ans = 1;
    while(b) {
        if(b & 1) ans = (ans * a) % m;
        b >>= 1; a = (a * a) % m;
    } 
    return ans;
}

int main(void) {
    a = read(), b = read(), m = read();
    printf("%lld", qpow(a, b, MOD));
    return 0;
}
//by CaO

上面的代码是基于 m 是质数且 a,m 互质的情况下给出的,但如果 m 不是质数呢?

例如 a=2,m=10,b=11,则 211mod10=8,但 211mod9mod10=22mod10=4,两者显然不相等。这个时候,直接用 Fermat 小定理就不可行了 QwQ。

但是 Subtask 3 又没有保证 m 一定是质数,更没有保证 am,怎么办呢?

这就是接下来,我们所要谈论的问题。

Euler 函数

我们定义 Euler 函数为 φ(n),其中 nNφ(n) 代表1n1 中,与 n 互质的整数的个数

例如 φ(12)=4,因为 111 中,与 12 互质的整数只有 1,5,7,11 四个。

怎么求 Euler 函数的值呢?我们当然可以暴力枚举 1n1 中所有的整数,找到其中与 n 互质的数的个数。枚举的复杂度为 Θ(n),判断是否互质的时间复杂度为 O(logn),故时间复杂度为 O(nlogn)

但假如 n109O(nlogn) 的时间复杂度很明显会超时 QwQ。

我们要想一种更快的求 Euler 函数的算法,但在这之前,我们应该先了解一下素数唯一分解定理

素数唯一分解定理:对于任意不小于 2 的正整数 n,都唯一存在一个严格递增质数数列 p1,p2,,pk,以及一个正整数数列 x1,x2,,xk,使得:n=p1x1p2x2pkxk=i=1kpixi
即:对任意一个不小于 2 的正整数,只有一种将其分解质因数的方法。

例如,72=23×32,而且没有其他的分解方式了。

这个定理非常显然,但是要证明则需要使用数学分析相关的知识,故在此不进行证明。

我们只是需要利用这个定理来帮助我们求 Euler 函数值:假如将 n 分解质因数:n=i=1kpixi,那么就有:

φ(n)=i=1kpik11(pi1).

当然,因为证明步骤太过繁琐,此处依旧不对该结论进行证明,有兴趣的读者可以自行阅读其他有关材料。

但是这个公式启发我们:如果我们需要求一个数的 Euler 函数,只需要对它分解质因数就可以了,因为分解质因数的算法是 O(n) 的,所以我们可以直接把 Euler 算法的时间复杂度优化到了 O(n)!QwQ

代码也很好写:

long long phi(long long n) {  //求 Euler 函数
	ll ans = n;
	for(int i = 2; i <= n; ++i) {
		if(n % i == 0) {
			ans = ans / i * (i - 1);
			while(n % i == 0) n /= i;
		}
	}
}

Euler 定理与扩展 Euler 定理

『CaO 你是在糊弄我们吗?一个破函数讲了半天,怎么优化快速幂还没告诉我们呢!』

别急~既然这个函数名字叫 Euler 函数,那么它肯定和那个大名鼎鼎的数学家 Euler 有关系。所以,Euler 也给出了一个非常著名的定理,即 Euler 定理

an 时,有 aφ(n)1(modn),其中 φ(n) 是 Euler 函数。

n 为质数时,上述定理即退化为 Fermat 小定理。所以该定理又名 Euler-Fermat 定理。

证明如下(数学证明警告!):
我们不妨设从 1n1 中,与 n 互质的整数分别为 x1,x2,xφ(n),那么一定有:

axiaxj(modn), if ijaZ.

所以有

aφ(n)x1x2  xφ(n)(ax1)(ax2)  (axφ(n))(ax1modn)(ax2modn)  (axφ(n)modn)x1x2  xn(modn)

两边同时消去 x1x2  xφ(n),即得到 aφ(n)1(modn)

这也就是说,即使 Subtask 3m 不保证是质数,如果保证 am,也可以通过 Euler 定理算出来,时间复杂度为 O(m+logm)=O(m)

然而并没有什么卵用我们可以轻松地构造出很多组 a,m,让 a,m 不互质。这个时候,Euler 定理的前提就不成立了,也就不能用 Euler 定理来加速原本的快速幂了。我们能否在 Euler 定理的基础上,得出一个更具有普适性的结论呢?

为了解决这样的问题,(据听说是)国内的某位 OIer,提出了扩展 Euler 定理

对于任意的正整数 a,m,都有 a2φ(m)aφ(m)(modm)

当然,我们也就可以得出 bφ(m) 时,ababmodφ(m)+φ(m)(modm)

证明如下(数学证明警告!):

不妨设 am 存在一个公共质因子 p,使得 m=pqr,其中 pq
那么由 Euler 定理得 pφ(q)1(modq).

又因为 q 质因子 p,故有 φ(q)φ(m),即 pφ(m)1(modq)

pkφ(m)1(modq)。即 pkφ(m)+rpr(modqpr)

pkφ(m)+r+cpr+c(modm)

上述结论可表述为:若 cr,则 pcpkφ(m)+c(modm)

不妨设 a 中含有素因子 pk,且存在 rφ(m)c

(pk)cpkcpkφ(m)+kcpk(φ(m)+c)(pk)φ(m)+c(modm)

(pk)c(pk)cmodφ(m)(modm)

对于 a 的每个素因子都成立,相乘即得:

acacmodφ(m)+φ(m)(modm).

这个定理给了我们一个启发:对于任意的 b,我们把普通的 Euler 定理和扩展 Euler 定理做一个整合,就可以得到这个公式:

abmodm={abmodmb<m and gcd(b,m)1,abmodφ(m)+φ(m)modmotherwise.

所以,无论 b 再怎么大,最后需要计算的指数都可以被这个定理限制在 2φ(m) 以内,这样,我们用快速幂计算的时间复杂度就不会超过 O(logφ(m))=O(logm) 了!还不如预处理 Euler 函数的 O(m) 的时间复杂度高。所以,项这样用扩展 Euler 定理加速的快速幂,又被戏称为光速幂

代码

虽然在洛谷上,它是一道蓝题,但写起来真的是很轻松 QwQ~

#include <iostream>
#include <cstring>
using namespace std;

typedef long long ll;
const int MAXN = 100000001;

ll a, b, m, phim;
char B[MAXN];
bool flag = false;  //判断 b 是否大于 phi(m)

ll phi(ll n) {  //欧拉函数
    ll ans = n;

    for(int i = 2; i <= n; ++i) {
        if(n % i == 0) {
            ans = ans / i * (i - 1);
            while(n % i == 0) n /= i;
        }
    }

    return ans;
}

ll qpow(ll a, ll b, ll m) {
    ll ans = 1;

    while(b) {
        if(b & 1) ans = (ans * a) % m;
        b >>= 1, a = (a * a) % m;
    }

    return ans;
}

int main(void) {
    cin >> a >> B >> m;
    phim = phi(m);

    for(int i = 0; B[i]; ++i) {
        b = (b * 10) + B[i] - '0';  //类似于快读的输入方式,可以达到边读入边取模的目的
        if(b >= phim) flag = true;
        b %= phim;
    }

    if(flag) printf("%lld", qpow(a, b + phim, m));  //b > phi(m) 时需要加一个 phi(m)
    else printf("%lld", qpow(a, b, m));  //否则直接上快速幂
    
    return 0;
}
//by CaO

关于 Euler 函数前缀和

当然,Euler 函数的用途不止于此。考虑这样一个问题:求在 [1,n] 中互质的整数有多少对。

这个问题也就可以形式化地表示为,给定整数 n,求下面这个式子中 S 的值:

S=x=1ny=1x[gcd(x,y)=1].

『这个我会!Θ(n2) 暴力枚举 x,y,然后 O(logn) 判断是否互质,总复杂度为 O(n2logn)!』

那么,当 n104 时呢?

『让我想想……哦,我明白了!我们考虑下面的这个式子:

i=1n[gcd(n,i)=1]

这个式子代表从 1n1 中,所有与 n 互质的数的个数。根据 Euler 函数的定义,这个式子就等于 φ(n),所以我们对待求的式子做变形,就可以变为:

x=1nφ(x).

我们可以 O(n) 处理出某个整数的 Euler 函数值,要求 n 个函数值,复杂度为 O(n32)。』

那我再放大一点数据,n107 呢?

我会杜教筛!还会 Möbius 反演,可快可快了!

……不你不会╮( ̄▽ ̄)╭

……为什么?

不要老是拆我的台啊 kora(ノ=Д=)ノ┻━┻这两个我还不会呢!

那……我会埃氏筛和 Euler 筛,这总行了吧……?

OK,可以,很好!我们可以利用筛法来在 Θ(nloglogn) 甚至 Θ(n) 的时间复杂度来解决 Euler 函数的前缀和问题。

我们先了解 Euler 函数的一个性质:

Euler 函数是一个积性函数。对于一个定义在 N 上的函数 f(x)f(x) 是积性函数,当且仅当 abf(ab)=f(a)f(b)

这也就是说:ab,都有 φ(ab)=φ(a)φ(b)

我们不妨考虑一个正整数 x,考虑将其分解质因数:x=i=1npiki,既然 φ(n) 是一个积性函数,又有 p1,p2,,pn 两两互质,所以一定有 φ(x)=i=1nφ(piki)

考虑到 φ(piki)=piki1(pi1)=pikipi1pi,所以有 φ(x)=xi=1npi1pi

我们可以预先设定所有的 φ(x)=x,然后用线性筛或者埃氏筛,筛出所有的质数,对每个质数 p,将它的所有倍数都乘上 p1p。这样,复杂度就被优化成了 Θ(n)Θ(nloglogn) 了!

代码如下:

#include <iostream>
using namespace std;

typedef long long ll;
const int MAXN = 10000001;

ll n, phi[MAXN];
int main(void) {
    scanf("%lld", &n);
    
    for(int i = 1; i <= n; ++i)  //初始化
        phi[i] = i;
    
    for(int i = 2; i <= n; ++i)  //这里用了埃氏筛,因为线性筛太难写了 QwQ
        if(phi[i] == i)  //如果 phi[i] == i,就意味着这个数没有被任何一个质数筛过,那么它就也是一个质数
            for(int j = i; j <= n; j += i)  //埃氏筛模板
                phi[j] = phi[j] * (n - 1) / n;  //对每个 j 是 i 的倍数,更新 phi[j] 的值
    //时间复杂度 O(n log log n),如果用线筛可以优化到 O(n)
    
    for(int i = 1; i <= n; ++i)  //线性求前缀和
        phi[i] = phi[i - 1] + phi[i];
        
    printf("%lld", phi[n]);
    return 0;
}
//by CaO

Euler 函数的应用

正如前言所提到的,Euler 函数在数学中有着非常广泛的应用。例如:

BSGS 算法

BSGS 算法可以解一类名叫离散对数的同余方程,即形如 axb(modm) 的方程。

根据 Euler 定理,当 am 的时候,aφ(m)1(modm)。那么这个时候,我们就可以将 x 的范围缩小到 1m。这个时候,再利用分块的思想就可以将它的复杂度优化到 O(m)

如果有机会,我会着重讲一下这个算法具体的实现。但是,在判断方程是否有解的时候,Euler 函数起到了非常重要的作用。

原根

对于两个正整数 ma,假如 am,那么一定有 aφ(m)1(modm)。这个时候,我们就可以把最小的满足 ax1(modm)最小的正整数 x 叫做 am,记作 x=δm(a)。显然,δm(a)φ(m),那么我们把满足 δm(a)=ϕ(m) 的整数 a 记作模 m 意义下的原根

有了 Euler 函数,我们才能够定义原根的概念,有了原根,才有了像 NTT 这样的高性能的算法。所以,Euler 函数对解决算法问题的作用,不止是光速幂这一点。

总结

Euler 函数的作用其实不止上面这些,还涵盖了群论方面的知识点,所以如果你想更深入地学习数论和群论相关的知识,学习 Euler 函数和 Euler 定理是一件非常有用的事!

例题

本题目列表会持续更新。

posted @   CaO氧化钙  阅读(364)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示