算法学习笔记(2): 欧拉定理与逆元

逆元

定义

逆元素,是指一个可以取消另一给定元素运算的元素

具体来说,对于实际的一些应用,如:

当我们想要求(11 / 3) % 10

明显可以看出,是没有办法直接算的,这时就需要引入逆元

a 在模p意义下的逆元记作 a1,也可以用inv(a)表示

应当满足

aa11(modp)

则此时,(11 / 3) % 10就可以写成(11 * inv(3)) % 10

可以求出,inv(3)在模10意义下= 7

3×inv(3)=21211(modp)

(11 / 3) % 10 = (11 * 7) % 10 = ((11 % 10) * (7 % 10)) = (1 * 7) % 10 = 7

为什么我要多此一举在第三步再变换一次?

在实际应用中,a * b可能会很大以至于溢出,导致错误,所以分开来乘以减小数据规模


如何求?

费马小定理

依据费马小定理(需要注意先决条件,ap互质且p是质数)

费马小定理可以通过欧拉定理求解,详见后文欧拉定理

gcd(a,p)=1 and p is primeapa(modp)

故此时可以有

a1ap2(modp)

扩展欧几里得算法

如果不满足先决条件呢?

这是相对来说的通法,但是总会有数据可以卡(不知道为什么

根据观察

a1a1(modp)

i=a1换成等式可以知道

ia+rp=1

由于已知a,p,则此时可以通过扩展欧几里得算法求解 i 的值

扩展欧几里得算法可以参考这篇文章:算法学习笔记(1): 欧几里得算法及其扩展


欧拉定理

再推广一下?若 p 不为质数呢?

那么就要有欧拉定理来了

gcd(k,p)==1kφ(p)1(modp)

φ(p)[1,p] 中与p互质的数的个数。特别的,1也算。

φ(p)=ord(Zp)

或者说, φ(p) 的大小就是模 p 的一个完全剩余系的大小

而完全剩余系满足运算封闭,所以下文中 A1=A2 也可换一种解释方法了

举个例子:

  • φ(7)=6 ,因为7是质数(所以在p为质数的时候就退化成费马小定理了)

  • φ(6)=2,因为只有1, 5和它互质

但是如何求φ(p)呢?

  1. p分解质因数,于是有 p=a1c1a2c2a3c3ancn

  2. 此时φ(p)=pi=1nai1ai

另外 φ(x) 为积性函数

gcd(x,y)=1φ(xy)=φ(x)φ(y)


欧拉定理证明

数论证明

令集合A[1,p] 中所有与p互质的数,即

A1={a1,a2,a3,,aφ(p)}

任取一个与 p 互质的数 k

A中每一个元素在模p意义下乘k,由于A中元素与p互质,且k也与p互质,可知

A2={ka1%p,ka2%p,ka3%p,,kaφ(p)%p}

也满足为 [1,p] 中所有与p互质的数,故可知 A1=A2

补充,为什么两者相等:

我们考虑在 A2 中取出 ka1ka2,并假设两者同余。即 ka1ka2(modp)

也就是 pk(a1a2)

由于 kp 互质,可以得到 p|(a1a2)

同时,由于 a1,a2 在数列 A1 中,意味着 |a1a2|<p

由于有且仅有一个实数 0 满足绝对值小于 p 且能被 p 整除

所以可以知道 ka1=ka2

也就是说,A2 中不存在相同的两个元素。同时,A2 是由 A1 中所有元素变化而来,也就是说 A2 是与 A1 等价的剩余系

经过重排序之后,两者相等

这里也可以看出,为什么 k 需要与 p 互质

于是

i=1φ(p)aii=1φ(p)kai(modp)

即是

i=1φ(p)aikφ(p)i=1φ(p)ai(modp)

左右相减,变形即可知 kφ(p)1(modp)

扩展-群论证明

2023/01/15 更新

考虑 kp 互质, 意味着 k 在模 p 的完全剩余系中

我们将之看成一个群, 并且可以知道, 模 p 的完全剩余系为一个循环群

那么t,kt1(modp), 此时, |<k>|  |ttk 的生成子群的大小的倍数)

根据某些定理:对于 kH,|<k>||H|

这个LaTeX格式太难看了QwQ。定理:循环群的大小一定是其生成子群大小的倍数

由于上文提及过 φ(p)=|H|

也就是说,s,st=φ(p)

那么可以推出 ktkφ(p)1(modp)

这里也可以体现为什么 k 必须与 p 互质

只有 kZp 中才能进行上述推论

扩展欧拉定理

没有互质的限制了

{k<φ(p)akak(modp)kφ(p)akakmodφ(p)+φ(p)(modp)

想必证明很简单,这里就不展开叙述了 其实是太复杂了,懒得写 QwQ

【模板】扩展欧拉定理 - 洛谷


补充:快速幂

可以看出,如果要利用欧拉定理,需要求ak,当k非常大的时候,就需要快速幂的帮助了

推荐阅读:快速幂

这里给出一种参考代码

// (a**x) % p
int quickPow(int a, int x, int p) {
    int r = 1;
    while (x) {
        // no need to use quickMul when p*p can be smaller than int64.max !!!
        if (x & 1) r = (r * a) % p;
        a = (a * a) % p, x >>= 1;
    }
    return r;
}

至于其中的那一行注释,主要是考虑到当a, p都很大(如:a = 1e15, p = 1e17 + 1时,a * a一定会溢出,所以需要“快速”乘来辅助)

实际上“快速”乘特别慢,是O(logn)的复杂度……所以叫龟速乘也不为过

推荐阅读:快速乘总结 - 一只不咕鸟,里面有更详细的阐述

这里给出快速乘的一种参考代码

// a*b % p O(log b)
int quickMul(int a, int b, int p) {
    // let b < a, to reduce a little time to process.
    if (a < b) std::swap(a, b);

    int r = 0;
    while (b) {
        if (b & 1) r = (r + a) % p;
        a = (a<<1) % p, b >>= 1;
    }
    return r;
}

notice: 适当的使用long long

线性求逆元

不妨设我们需要求i在模p意义下的逆元

很容易知道,1的逆元为1,所以边界条件就有了

p=ki+r, 放在模 p 意义下则有 ki+r0(modp)

两边同时乘以 i1r1 可以得到 kr1+i10(modp)

变换一下

i1kr1(modp)i1pi(p mod i)1(modp)inv(i)(ppi)inv(p%i)(modp)

所以,有了递推式

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

线性求阶乘逆元

这个东西一般用于求组合数

我们先预处理出阶乘

fac[0] = 1;
for (int i = 1; i <= n; ++i)
    fac[i] = (fac[i - 1] * i) % p;

根据逆元定义i 1i1(modp)

所以 inv(i!)1i!(modp)

稍微变换一下

1i!1(i+1)!(i+1)(modp)

所以有了递推式

ifac[i] = ifac[i + 1] * (i + 1) % p

我们逆着推,假设最大需要到n

ifac[n] = quickPow(fac[n], p - 2);
for (int i = n; i; i--)
    ifac[i - 1] = ifac[i] * i % p;

同时求逆元与阶乘逆元

还是逆元的本质是求倒数

inv(i)1i(modp)

稍微变换一下

inv(i)1i!(i1)!inv(i!)(i1)!(modp)

所以

inv[i] = ifac[i] * fac[i - 1] % p

合起来就是

for (int i = n; i; i--) {
    inv[i] = ifac[i] * fac[i - 1] % p;
    ifac[i - 1] = ifac[i] * i % p;
}

就可以在较少的常数下同时求得两者了

注意:如果模数小于要求的数的范围,那么……自求多福 QwQ

posted @   jeefy  阅读(412)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示