专栏文章
[数学]乘法逆元
算法·理论参与者 50已保存评论 65
文章操作
快速查看文章及其快照的属性,并进行相关操作。
- 当前评论
- 65 条
- 当前快照
- 1 份
- 快照标识符
- @mhz5s50k
- 此快照首次捕获于
- 2025/11/15 01:55 4 个月前
- 此快照最后确认于
- 2025/11/29 05:24 3 个月前
0.前言
在 中,大多数情况下,善良的出题人为了避免高精度等大整数计算,常常会要求输出答案对一个数(大多是质数)取模的情况,但这衍生了一个问题:若题目中计算需用到除法而我们知道,如果 在大部分情况下 (注意一般题目会默认对一个数取模是数论(整数)意义上的取模,故这里除法为整数除法),这和等式的性质是不同的,要解决这个问题,就需要用到一个概念:乘法逆元。
本文主要介绍乘法逆元的定义以及其求法QwQ
1.定义
如果您到百度上搜索逆元,您会看到如下定义:(这里逆元素是逆元的全称)
逆元素是指一个可以取消另一给定元素运算的元素---百度百科
这个定义似乎不太那么直观……
我们来举个例子吧,先再实数范围举例,由小学知识可知,如果一个代数式 乘一个数 后,再乘它的倒数 ,相当于没有乘 (这里不考虑 的情况),换句话说,我们乘 后,取消了代数式 乘 后值增大的影响。
不难发现这符合逆元的定义,故我们可以说一个数和其倒数互为乘法逆元。除此之外,我们还能发现一个数和其相反数互为加法逆元等等……
接下来回到代数式的例子,考虑为什么 的倒数 能消去乘 的影响。显然,是由于乘法结合律的存在,使得我们在运算 时可以先运算 的值,再运算它和 的乘积,而 ,任何数与 的乘积均为其本身,从而使乘 对 值增大的影响取消。
上文在实数范围内讨论了乘法逆元,现在我们来看本文主要讨论的内容,数论模意义下的乘法逆元。
由上文的分析来看,我们可以借助“任何数与的乘积均为其本身”的性质定义模意义下的乘法逆元。
故其定义为:如果说 在模 意义下的乘法逆元是 ,那么
我们通常将 的逆元记作 。
如同 没有倒数一样, 也没有乘法逆元。
不难发现, 在模 意义下的乘法逆元均为 中的整数。
2.求逆元的方法
1.拓展欧几里得求逆元
不难发现 可以写成 的形式:
将同余号换为等号得:
移项得:
由上式可知 为 的整数倍,不妨设其为 的 倍:
继续移项得:
令 ,就写成了上文的形式:
我们可以通过不断迭代的方式来求解单个数的逆元,这里涉及到了解线性同余方程的知识,可以看我的另一篇文章
我们发现这个方程有解的条件是 ,即 , 互质,所以得出结论: 在模 意义下的乘法逆元存在当且仅当 互质,这也在一定程度上解释了大多数题目模数为什么为质数的原因:(设模数为 )可以保证在 中的所有整数都有模 意义下的逆元。
而由于 ,故我们可以直接运用拓展欧几里得算法解 这个方程。
时间复杂度为 。
模板题参考代码:
CPP#include <cstdio>
#include <cstring>
#include <iostream>
using namespace std;
long long a, b;
int exgcd(long long a, long long b, long long &x, long long &y)
{
if (b == 0)
{
x = 1, y = 0;
return a;
}
int d = exgcd(b, a % b, y, x);
y -= a / b * x;
return d;
}
int main()
{
scanf("%lld%lld", &a, &b);
long long x = 0, y = 0;
exgcd(a, b, x, y);
printf("%lld", (x % b + b) % b);//这里防止出现负数
return 0;
}
2.欧拉定理求逆元
观察式子 ,我们发现它和欧拉定理 很像……
而欧拉定理又是 互质时成立,这和逆元存在的条件相同……
故将两个式子合在一起,得:
两遍同除得:
故
我们可以在 的时间内求出单个数的欧拉函数。
引理:设 为正整数 的质数幂乘积表示式,则:
证明:
由于各质数幂之间是互质的,根据欧拉函数的性质可得:
进而得证。
在求一个数的欧拉函数时,不妨将上式后面的分数部分通分化作 ,我们可以在 的时间内枚举 的每个质因子,初始化 为 ,每枚举到一个质因子 就让 ,发现每个质因子只对答案贡献一次,故在统计完该质因子的答案后,这个质因子就无用了,为了方便最后判断我们是否将的所有质因子全都枚举到,我们可以通过不断除该质因子来消去它,防止其再被计入答案。
求出欧拉函数后,可以使用快速幂求出答案。
故这样做的时间复杂度为 。
模板题参考代码:
CPP#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
long long n, p;
long long phi(long long x)//求x的欧拉函数
{
long long res = x, tmp = x;//初始化答案为x
for (int i = 2; i * i <= tmp; ++i)
{
if (x % i == 0)
{
res = (res / i) % p * ((i - 1) % p) % p;//找到x的一个质因子,计算其对答案的贡献
while (x % i == 0) x /= i;//统计完答案后,除去该质因子
}
}
if (x > 1) res = (res / x) % p * ((x - 1) % p) % p;//防止漏筛质因子
return res;
}
long long power(long long a, long long b)//快速幂
{
long long res = 1;
while (b)
{
if (b & 1) res = res * a % p;
a = a * a % p;
b >>= 1;
}
return res;
}
int main()
{
scanf("%lld%lld", &n, &p);
long long tmp = phi(p) - 1;
printf("%lld", power(n, tmp));
return 0;
}
3.费马小定理求逆元
注意,这种求逆元的方法仅使用在 为质数的时候
费马小定理:若 为质数,且整数 满足 ,则有
对于定理中的式子,我们可以将两边同除,得:
而 就是 在模 意义下的乘法逆元。
故我们只需计算 即可,而这个过程可用快速幂解决。
时间复杂度为
我们不妨看看本文开篇提出的问题,如何对一个分数取模?
一般的讲,我们是这么定义分数取模的:
我们设 的值为一个整数 ,其满足 且 。
我们只需求出 的值即可。而上式两边同乘 在模 意义下的乘法逆元 后可得:
而 的范围在模 剩余系内,故可直接写作
在本题中,,是个质数,故可以直接使用费马小定理求逆元。
但注意:
当 时,费马小定理不成立,故无解。
本题 数据范围很大, 也存不下,故需要写快读,边读边取模。
模板题参考代码:
CPP#include <cstdio>
#include <cstring>
#include <cctype>
#include <algorithm>
#include <iostream>
using namespace std;
const int p = 19260817;
template<typename T>
inline void input(T &x)
{
x = 0;
char ch = getchar();
bool f = false;
while (!isdigit(ch))
{
f |= (ch == '-');
ch = getchar();
}
while (isdigit(ch))
{
x = (x << 1) + (x << 3) + (ch ^ 48);
x %= p;//边读边取模
ch = getchar();
}
x = f ? -x : x;
}
inline int power(int a, int b)//快速幂
{
int res = 1;
while (b)
{
if (b & 1) res = 1ll * res * a % p;
a = 1ll * a * a % p;
b >>= 1;
}
return res;
}
int main()
{
int a, b;
input(a), input(b);
if (!b)
puts("Angry!");//b % p = 0 时无解
else
printf("%d", (int)(1ll * a * power(b, p - 2) % p));
return 0;
}
4.线性求逆
注意,这种求逆元的方法仅使用在 为质数的时候
基于拓展欧几里得算法,我们虽然可以在 时间内,求出 中所有整数在模质数 下的乘法逆元,但在面对更大的数据范围时,我们就需要更快的方法。
首先,我们不难发现,对于任何正整数 ,,即 在模任意正整数意义下的逆元为其本身。
然后设 ,将其转化为同余式会得到:
两边同时乘 ,得:
将式子乘开:
移项得:
由 易知 ,故有:
而 ,故 的逆元我们之前已经递推出来了,故我们可以通过这个式子推出 的逆元是谁。
综上,我们在已知 的逆元为 的情况下,可以推出 中所有整数在模质数 下的乘法逆元。
这个算法的时间复杂度为 。
注意 为负数,但在模 意义下,其等价于 ,故在实际应用中真正的递推式为:
模板题参考代码:
CPP#include <cstdio>
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int maxn = 3e6 + 10;
long long inv[maxn], n, p;
int main()
{
scanf("%lld%lld", &n, &p);
inv[0] = 0, inv[1] = 1;//初始化1的逆元为1
printf("%lld\n", inv[1]);
for (int i = 2; i <= n; ++i)
{
inv[i] = (p - p / i) * inv[p % i] % p;//上文中的式子
printf("%lld\n", inv[i]);
}
return 0;
}
5.离线逆元
在 2019 年十二省联考中某个 std 长达 800~~(900?)~~ 多行的题中用到了这个新知识。
这个算法用途是:对于给定的 个整数 ,求它们在模 意义下的乘法逆元。
的递推显然不适用,时间空间双双爆炸。
基于拓展欧几里得算法或费马小定理的 的朴素算法也会超时,我们需要一种接近线性的算法。
我们不妨设 为 的前缀积(即 )
然后我们就用到了一个 :前缀积的逆元就是逆元的前缀积(即逆元是完全积性的)
我们不妨来证明一下逆元是完全积性的,即证明对于任意正整数 ,它们在模 意义下的逆元满足:
根据逆元的定义, 在模 意义下的逆元满足:
将两个式子相乘得:
不难发现上式中 符合 的定义,于是 ,从而得证。
故我们利用费马小定理求出的 的逆元,它其实是 的前缀积(即 )
而 ,故我们求出可以从开始,在求出 后,利用 递推出 ,再用前文的式子递推出 即可。
综上,递推式为:
(其中 已求出,)
这种求逆元的方法的时间复杂度为 ,已经接近线性。
模板题参考代码:(注意常数优化和防爆 )
CPP#include <cstdio>
#include <cstring>
#include <cctype>
#include <iostream>
#include <algorithm>
using namespace std;
template<typename T>
inline void input(T &x)//本题卡常数,需要用快读
{
x = 0;
register char ch = getchar();
register bool f = false;
while (!isdigit(ch))
{
f |= (ch == '-');
ch = getchar();
}
while (isdigit(ch))
{
x = (x << 1) + (x << 3) + (ch ^ 48);
ch = getchar();
}
x = f ? -x : x;
}
const int maxn = 5e6 + 10;
int n, p, k, a[maxn], inv[maxn], pre[maxn], inv_pre[maxn];
inline int power(int a, int b, int p)
{
int res = 1;
while (b)
{
if (b & 1) res = 1ll * res * a % p;
a = 1ll * a * a % p;
b >>= 1;
}
return res;
}
int main()
{
input(n), input(p), input(k);
pre[0] = 1;//初始化
for (register int i = 1; i <= n; ++i)
{
input(a[i]);
pre[i] = 1ll * pre[i - 1] * a[i] % p;//计算前缀积
}
inv_pre[n] = power(pre[n], p - 2, p);//求出前缀积的逆元
for (register int i = n; i > 0; --i)//递推求逆元
{
inv[i] = 1ll * pre[i - 1] * inv_pre[i] % p;
inv_pre[i - 1] = 1ll * inv_pre[i] * a[i] % p;
}
int ans = 0;//计算题目中式子的值
for (register int i = 1, t = k; i <= n; ++i)
{
ans = (1ll * inv[i] * t % p + ans) % p;
t = 1ll * t * k % p;
}
printf("%d", ans);
return 0;
}
3.后记
本文部分参考了《信息学奥赛之数学一本通》(东南大学出版社)中的内容。
感谢 @Wallace 指出本文在分析“欧拉定理求逆元”时的时间复杂度上的错误。
感谢 @Liuier 指出本文推导“离线逆元”的式子时出现的错误。
当 不是质数时,线性递推求模 意义下的逆元的方法被证明是错误的……决定撤下QAQ
补充了逆元的完全积性的证明QwQ
感谢大家的阅读,再会!(^_−)☆
相关推荐
评论
共 65 条评论,欢迎与作者交流。
正在加载评论...