逆元

乘法逆元

\(ax \equiv 1 \pmod {p}\),则这个 \(x\) 也叫做在模 \(p\) 意义下 \(a\) 的逆元。乘法逆元就是模意义下的除法。

求解乘法逆元的常见方法:

  1. 当模数为质数时:使用费马小定理,当 \(p\) 为质数时,\(a^{p-1} \equiv 1 \pmod {p}\)。因此 \(a\)\(p-2\) 次方模 \(p\) 即为 \(a\) 在模 \(p\) 意义下的逆元。用快速幂实现,时间复杂度 \(O(\log p)\)
  2. 当模数不是质数,但 \(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}\) 都是已知的,然后:

  1. 利用 \(a_i^{-1} = s_{i-1} \times s_i^{-1} \bmod p\) 计算出当前 \(a_i\) 的逆元。
  2. 利用 \(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;
}
posted @ 2025-07-10 20:56  RonChen  阅读(60)  评论(0)    收藏  举报