逆元
乘法逆元
若 \(ax \equiv 1 \pmod {p}\),则这个 \(x\) 也叫做在模 \(p\) 意义下 \(a\) 的逆元。乘法逆元就是模意义下的除法。
求解乘法逆元的常见方法:
- 当模数为质数时:使用费马小定理,当 \(p\) 为质数时,\(a^{p-1} \equiv 1 \pmod {p}\)。因此 \(a\) 的 \(p-2\) 次方模 \(p\) 即为 \(a\) 在模 \(p\) 意义下的逆元。用快速幂实现,时间复杂度 \(O(\log p)\)。
- 当模数不是质数,但 \(a\) 和 \(p\) 互质时,使用扩展欧几里得算法求解,时间复杂度 \(O(\log \max (a,p))\)。
选择题:考虑一个自然数 \(n\) 以及一个模数 \(m\),你需要计算 \(n\) 的逆元(即 \(n\) 在模 \(m\) 意义下的乘法逆元)。下列哪种算法最为适合?
- A. 使用暴力法依次尝试
- B. 使用扩展欧几里得算法
- C. 使用快速幂法
- D. 使用线性筛法
答案
B
习题:P2613 【模板】有理数取余
解题思路
看起来是输入都是大数,但实际上 \(a\) 和 \(b\) 都对模数取模不会影响结果。
用字符串读入,一位一位把数拼起来,在乘加的过程中取模。
因为 \(19260817\) 是质数,所以此时无解的情况就是 \(b\) 在对 \(19260817\) 取模后为 \(0\)。
要计算的结果就是 \(a\) 乘上 \(b\) 的逆元,这里的 \(a\) 和 \(b\) 都是初始值取过模的,因为 \(19260817\) 是质数,可以直接用费马小定理,\(b\) 的逆元就是 \(b\) 的 \(19260815\) 次方再对模数取模。
参考代码
#include <cstdio>
const int LEN = 10005;
const int MOD = 19260817;
char a[LEN], b[LEN];
int qpow(int x, int y) {
int res = 1;
while (y > 0) {
if (y & 1) res = 1ll * res * x % MOD;
x = 1ll * x * x % MOD;
y >>= 1;
}
return res;
}
int main()
{
scanf("%s%s", a, b);
int x = 0;
for (int i = 0; a[i]; i++) {
x = x * 10 + a[i] - '0';
x %= MOD;
}
int y = 0;
for (int i = 0; b[i]; i++) {
y = y * 10 + b[i] - '0';
y %= MOD;
}
if (y == 0) {
printf("Angry!\n");
} else {
int ans = 1ll * x * qpow(y, MOD - 2) % MOD;
printf("%d\n", ans);
}
return 0;
}
线性求逆元
如果需要求从 \(1\) 到 \(n\) 所有数字在模 \(p\) 意义下的乘法逆元,那么根据费马小定理,可以一个一个求解,总的时间复杂度是 \(O(n \log p)\)。但当 \(n\) 和 \(p\) 很大时,这个速度就不够快了。而线性递推法可以把时间复杂度优化到 \(O(n)\),也就是“线性”时间。
线性递推的核心思想是:利用已经求出来的、较小的数的逆元,来推导出当前数的逆元。
假设要求 \(i\) 的逆元,记作 \(i^{-1}\),希望用 \(1^{-1}, 2^{-1}, \dots, (i-1)^{-1}\) 中的某些值来计算。
怎么建立这个关系呢?这里需要用到带余除法。对于 \(p\) 和 \(i\)(其中 \(1 \lt i \lt p\)),可以把 \(p\) 表示成 \(i\) 的倍数加上一个余数的形式:\(p = k \times i + r\)。其中,\(k\) 是商,也就是 \(\left \lfloor \dfrac{p}{i} \right \rfloor\),\(r\) 是余数。并且,余数 \(r\) 肯定比除数 \(i\) 要小,即 \(0 \le r \lt i\)。
现在,把上面这个式子放到模 \(p\) 的世界里来看。\(k \times i + r \equiv 0 \pmod {p}\),因为 \(p\) 本身除以 \(p\) 的余数就是 \(0\)。
然后移项:\(k \times i \equiv -r \pmod {p}\)。
关键的一步:想求 \(i\) 的逆元 \(i^{-1}\),想办法把它凑出来。在等式两边同乘以 \(i^{-1} \times r^{-1}\),得到 \((k \times i) \times (i^{-1} \times r^{-1}) \equiv -r \times (i^{-1} \times r^{-1}) \pmod {p}\),整理得到 \(k \times (i \times i^{-1}) \times r^{-1} \equiv -(r \times r^{-1}) \times i^{-1} \pmod {p}\)。
由于 \(x \times x^{-1} \equiv 1 \pmod {p}\),所以得到 \(k \times r^{-1} \equiv -i^{-1} \pmod {p}\)。
两边再乘以 \(-1\),就得到了 \(i^{-1}\) 的表达式:\(i^{-1} \equiv -k \times r^{-1} \pmod {p}\)。
把 \(k\) 和 \(r\) 用 \(p\) 和 \(i\) 代回去,得到 \(i^{-1} \equiv - \left \lfloor \dfrac{p}{i} \right \rfloor \times (p \bmod i)^{-1} \pmod {p}\)。
也就是说,要求 \(i^{-1}\),只需要知道 \((p \bmod i)^{-1}\) 就行了。因为 \(r = p \bmod i\) 肯定小于 \(i\),所以 \((p \bmod i)^{-1}\) 在计算 \(i^{-1}\) 之前就已经计算好了。这就构成了完美的递推关系。
递推的起点(边界条件)是什么? 需要知道 \(1^{-1}\),显然等于 \(1\)。
处理负数:在 \(\bmod p\) 的运算中,\(-x\) 等价于 \(p-x\),所以 \(- \left \lfloor \dfrac{p}{i} \right \rfloor\) 可以写成 \(p - \left \lfloor \dfrac{p}{i} \right \rfloor\),这样可以避免在编程时出现负数。
最终这个递推式为:\(i^{-1} = \left( p - \left \lfloor \dfrac{p}{i} \right \rfloor \right) \times (p \bmod i)^{-1} \bmod p\)。
习题:P3811 【模板】模意义下的乘法逆元
参考代码
#include <cstdio>
const int N = 3000005;
int inv[N];
int main()
{
int n, p; scanf("%d%d", &n, &p);
inv[1] = 1; printf("1\n");
for (int i = 2; i <= n; i++) {
inv[i] = 1ll * (p - p / i) * inv[p % i] % p;
printf("%d\n", inv[i]);
}
return 0;
}
线性求任意 \(n\) 个数的逆元
假设要对 \(p\) 取模,求 \(n\) 个数 \(a_1, a_2, \dots, a_n\) 的逆元 \(a_1^{-1}, a_2^{-1}, \dots, a_n^{-1}\)。
之前的方法 \(i^{-1} = \left( p - \left \lfloor \dfrac{p}{i} \right \rfloor \right) \times (p \bmod i)^{-1} \bmod p\) 在这里就失效了,因为它依赖于 \(i\) 和 \(p \bmod i\) 之间的大小关系,而任意给定的数不具备这种良好的递推结构。
第一步:计算前缀积
先计算这 \(n\) 个数的前缀积,存入一个数组 \(s\) 中:
- \(s_1 = a_1\)
- \(s_2 = a_1 \times a_2 = s_1 \times a_2\)
- \(s_3 = a_1 \times a_2 \times a_3 = s_2 \times a_3\)
- \(\dots\)
- \(s_i = s_{i-1} \times a_i\)
- \(\dots\)
- \(s_n = a_1 \times a_2 \times \cdots \times a_n = s_{n-1} \times a_n\)
这个过程是一个简单的 for 循环,时间复杂度是 \(O(n)\)。
第二步:求总乘积的逆元
现在先只求 \(s_n\) 的逆元,这里可以使用熟悉的快速幂(基于费马小定理)来求它:\(s_n^{-1} = s_n^{p-2} \bmod p\)。
这一步的时间复杂度是 \(O(\log p)\)。
第三步:倒序计算每个数的逆元(最关键的一步)
现在有了 \(s_n^{-1}\),也就是 \((a_1 \times a_2 \times \cdots \times a_n)^{-1}\)。
-
由于 \(s_n = s_{n-1} \times a_n\),所以 \(a_n^{-1} = s_{n-1} \times s_n^{-1} \bmod p\)。
-
由于 \(s_n = s_{n-1} \times a_n\),所以 \(s_{n-1}^{-1} = a_n \times s_n^{-1} \bmod p\),这里用 \(s_n^{-1}\) 推导出了 \(s_{n-1}^{-1}\)。有了 \(s_{n-1}^{-1}\),就可以像上面一样求 \(a_{n-1}^{-1} = s_{n-2} \times s_{n-1}^{-1} \bmod p\)。
规律:将 \(i\) 从 \(n\) 倒着循环到 \(1\),在循环的每一步,\(s_i^{-1}\) 都是已知的,然后:
- 利用 \(a_i^{-1} = s_{i-1} \times s_i^{-1} \bmod p\) 计算出当前 \(a_i\) 的逆元。
- 利用 \(s_{i-1}^{-1} = s_i^{-1} \times a_i \bmod p\) 计算出下一步循环需要的 \(s_{i-1}^{-1}\)。
这样,只需要一个倒序的 for 循环,就能算出所有数的逆元,时间复杂度是 \(O(n)\)。
总时间复杂度:\(O(n + \log p)\),是线性的。
习题:P5431 【模板】模意义下的乘法逆元 2
本题需要快读。
解题思路
\(k\) 的 \(i\) 次方是可以线性递推的,所以只需要处理出 \(a_i\) 的逆元,本题数据范围较大,需要线性求每个数的逆元。
参考代码
#include <cstdio>
const int N = 5000005;
int a[N], s[N], inv_s[N], inv_a[N];
int readint() {
int x = 0, f = 1; char ch = getchar();
while (ch < '0' || ch > '9') {
if (ch == '-') f = -1;
ch = getchar();
}
while (ch >= '0' && ch <= '9') {
x = x * 10 + ch - '0';
ch = getchar();
}
return x * f;
}
int qpow(int x, int y, int p) {
int res = 1;
while (y > 0) {
if (y & 1) res = 1ll * res * x % p;
x = 1ll * x * x % p;
y >>= 1;
}
return res;
}
int main()
{
int n = readint(), p = readint(), k = readint();
s[0] = 1;
for (int i = 1; i <= n; i++) {
a[i] = readint();
s[i] = 1ll * s[i - 1] * a[i] % p;
}
inv_s[n] = qpow(s[n], p - 2, p);
for (int i = n; i >= 1; i--) {
inv_a[i] = 1ll * inv_s[i] * s[i - 1] % p;
inv_s[i - 1] = 1ll * inv_s[i] * a[i] % p;
}
int ans = 0, ki = 1;
for (int i = 1; i <= n; i++) {
ki = 1ll * ki * k % p;
ans = (ans + 1ll * ki * inv_a[i] % p) % p;
}
printf("%d\n", ans);
return 0;
}

浙公网安备 33010602011771号