模算术

同余式

设 。称 是 mod模 同余的,如果 ;此关系也写作

易见 mod 同余是 上的等价关系:诚然

  • (因为 ,或者说 );
  • 等价于 (因为 等价于 ,或者说 对运算 封闭);
  • 和 蕴含 (因为 对加法封闭)。

同余类

选定 ,记 对等价关系 mod 同余的商集为 或简记为 ; 其中的等价类也称为 mod 同余类。

给定 ,包含 的同余类可以具体地被描述为 的子集

。

以后我们也会使用更简短的符号如 或 等来标记含有 的 mod 同余类。

在 的前提下运用带余除法,对每个 取其除以 的余数,记为 ,则有

此即“同余”之义。于是 mod 的同余类和 的元素一一对应,这就将 划分为 个 mod 同余类,以 为具体的代表元。

练习

证明若 ,,则有

练习

设 是整数,满足 。证明对任意整数 , 蕴含 。

证明:对同余式 两边同乘 得 。
对 两边同乘 ,得 ;同理,。根据同余关系的传递性,遂有 。

我们说 对乘法满足消去律。

同余式

作为同余式的初步例子,我们将介绍称为费马小定理的著名结果。作为准备,我们来研究同余式 对哪些 有解;当然,答案仅依赖于 所属的 mod 同余类。

设 。对于任意 ,我们有

证明 左式有解相当于说存在 使得 ,亦即 。将此代入裴蜀定理。

费马小定理

设 为素数,则对于所有 都有

作为推论,所有 都满足 。

证明 设 。则 ,存在 使得 。现在考虑 的所有倍数。如果 ,对两边同乘以 ,可得 。另一方面,素数的性质确保 时,。这一切表明

两两互不同余,而且都不 ,所以它们的同余类和 的同余类仅差一个重排。于是有

项项

因为 ,仿照之前办法可从同余式两边消去 ,这就证明了第一部分。

对于一般的 ,或者 ,从而对 两边同乘以 给出 ;或者 ,从而 平凡地成立(两边皆同余 ),这就证明了第二部分。

费马小定理的逆命题不成立

对所有 都有 但不是素数的正整数 称为 Carmichael 数,有无穷多个;前五个 Carmichael 数是 。尽管存在这些反例,上述性质仍然在一些概率素性检测中扮演要角。

欧拉函数

选定 。回忆到一个整数与 互素与否仅依赖于它的 mod 同余类。细观上面两个命题的证明,可以发现与 互素的同余类在 中占有特别的地位。

设 ,定义 为不超过 而与 互素的正整数的个数。

由于 可以通过代表元等同于 ,欧拉函数 正是与 互素的 mod 同余类个数。注意到 。

不难看出,

  • 当且仅当 是素数。
  • 设 是素数,,则 。

模算术

有些问题的答案可能非常大,以至于无法用编程语言内置的整数类型来表示,此时往往要求我们计算答案除以给定正整数 的余数。

对于这样的问题,我们在做整数的加减乘除运算时,只关心每次运算的结果除以 的余数。这样的算术系统称为模算术modular arithmetic。

在模 的算术系统里,不区别模 同余的数。我们可以当作其中只有 这 个不同的数。

模 的算术系统的加减乘法表

加 0 1 2 3 4
0 0 1 2 3 4
1 1 2 3 4 0
2 2 3 4 0 1
3 3 4 0 1 2
4 4 0 1 2 3
减 0 1 2 3 4
0 0 4 3 2 1
1 1 0 4 3 2
2 2 1 0 4 3
3 3 2 1 0 4
4 4 3 2 1 0
乘 0 1 2 3 4
0 0 0 0 0 0
1 0 1 2 3 4
2 0 2 4 1 3
3 0 3 1 4 2
4 0 4 3 2 1

例题 P7909 分糖果


给定三个正整数 。
整数 满足 。
求 除以 的余数的最大值。

数据范围:

解法:

  • 我们把 除以 的余数记作 .
  • 答案是 .

模算术的实例

模算术有实际的例子,比如,手表的分秒读数是模 的算术系统,但是只有加 操作;更重要的例子是计算机里的有符号整数。

C++里的 int 类型是模 的算术系统:

  • 一个 int 被表示为一个长度为 的比特串,把一个比特的状态对应到数字 和 ,比特串就成为 位二进制数,数的范围是 到 。
  • 把 到 对应到 到 ,把 到 对应到 到 。

我们说 的编码是 个。这种编码方法称为补码。

练习

位有符号整数能表示的整数的范围是 。

设整数 满足 ,把 的编码的每一位取反,然后加一,就得到 的编码。

说明这种方法为何正确。

提示: 对应的 位二进制数是 。

实例研究:有符号整数的移位运算

  • -100 << 3 的值是 。
  • -100 >> 3 的值是 。

规则

  • 左移 位,结果的后 位用零填充。
  • 右移 位,结果的前 位用原来的最高位填充。

性质

设 是 位有符号整数(对于 short,,对于 int,,对于 long long,),而整数 满足 。按照前述规则,对左移位运算,易见

对于右移位运算,若 , 的最高位是 ,有

若 ,则有

模算术与除法

考虑如下场景:设 是整数, 且 。我们要计算 除以 的余数,但是 或 太大,先算出 , 再做除法是不能承受的。

若 互素,我们可以用下述方法解决此问题。

由于 互素,存在整数 使得 。设 ,于是有

左边乘 ,右边乘 ,给出

也就是说可以把算 变成算 。我们称 为 在模 下的乘法逆元。

在模 下的乘法逆元不唯一,但是根据前面证明的消去律,它们都 mod 同余。换言之,在 mod 意义下,乘法逆元唯一。

例子:模 下的乘法逆元

0 1 2 3 4 5 6 7 8
乘法逆元

模 意义下的

即便 不是整数,若 互素,则存在无穷多个整数 使得 ,并且这样的 模 都同余,我们写

在模 的意义下,我们可把整数 等同于分数 。

求模 逆元的方法

设 为素数,,我们求 在模 下的逆元,记作 。

法一 根据费马小定理,有

取 即可。用快速幂算法计算 ,时间是 。

int inverse(int x, int p) {
    int ans = 1;
    int n = p - 2;
    while (n > 0) {
        if (n & 1) ans = (long long) ans * x % p;
        x = (long long) x * x % p;
        n >>= 1;
    }
    return ans;
}

法二 的逆元是 。对 ,用带余除法把 表为 ,其中 。注意到

两边取逆元,得

两边乘以 给出

据此我们得到一个求 的递归算法:

int inverse(int x, int p) {
    if (x == 1) return 1;
    return p - (long long) (p / x) * inverse(p % x) % p;
}

法二的时间复杂度

int inverse(int x, int p) {
    if (x == 1) return 1;
    return p - (long long) (p / x) * inverse(p % x) % p;
}

此算法的运行时间不易分析,已知是 ,若不需要算很多次乘法逆元,可用它。若用它算很多次乘法逆元,可能超时,例如 B3717 组合数问题,这份代码就超时了。

我们建议优先使用法一。

上面我们得到递推式

虽然用它计算单个数的逆元,时间不易分析,却可以用它计算每个 在模 下的逆元:

int inverse_table(int p) {
    vector<int> inverse(p);
    inverse[1] = 1;
    for (int x = 2; x < p; x++)
        inverse[x] = p - (long long) (p / x) * inverse[p % x] % p;
}

用扩展欧几里得算法求逆元

一般来说,模数未必是素数。

回忆到对于整数 ,扩展欧几里得算法给出整数 使得 。若 ,此 恰是 在模 下的乘法逆元。

int inverse(int a, int n) { //计算a在模n下的逆元。要求a,n非负。
    int x, y;
    int d = extgcd(a, n, x, y); // d是gcd(a,n)
    assert(d == 1); // 确保 a,n 互素
    return x;
}

求逆元的非递归写法

// 求a在模n下的乘法逆元
int inverse(int a, int n) {
    int u = 0, v = 1;
    while (a != 0) {
        int q = n / a;
        n -= q * a;
        u -= q * v;
        swap(n, a);
        swap(u, v);
    }
    assert(n == 1);
    return u; // u可能为负数
}

用辗转相除法求 的过程可表为序列

其中每一项都可写成 的形式,

便是 在模 下的乘法逆元。

习题 P3811 模意义下的乘法逆元

给定正整数 和素数 ,求每个 在模 下的乘法逆元。

限制
int main() {
    int n, p;
    cin >> n >> p;
    vector<int> inv(n + 1);
    inv[1] = 1;
    for (int i = 2; i <= n; i++)
        inv[i] = p - (long long) (p / i) * inv[p % i] % p;
    for (int i = 1; i <= n; i++)
        cout << inv[i] << '\n';
}

习题 P5431 模意义下的乘法逆元 2

给定正整数 ,正整数 和素数 ,求 ,模 。

限制
  • ⏲️ 550 ms

解法

这题的时限只有 550 ms,不能对每个 都求逆元。考虑通分,把 写成

于是

不难想到下述解法:

  • 先计算 的后缀积,然后就能在 时间求出 。
  • 求 的逆元。

代码

int inverse(int x, int p) {
    if (x == 1) return 1;
    return p - (long long)(p / x) * inverse(p % x, p) % p;
}

int main() {
    ios::sync_with_stdio(0); cin.tie(0);

    int n, p, k; cin >> n >> p >> k;
    vector<int> a(n);
    for (int i = 0; i < n; i++) cin >> a[i];

    vector<int> b(n); //后缀积
    b[n - 1] = 1;
    for (int i = n - 1; i >= 1; i--) b[i - 1] = (long long)b[i] * a[i] % p;

    long long sum = 0, t = 1, prod = 1;

    for (int i = 0; i < n; i++) {
        prod = prod * k % p;
        sum += t * prod % p * b[i] % p;
        t = t * a[i] % p;
    }
    cout << sum % p * inverse((int)t, p) % p << '\n';
}

常数优化

考虑用递推法计算

对于 ,令

所求正是 。有递推式

这样就无需求序列 的后缀积了。

代码

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);

    int n, p, k;
    cin >> n >> p >> k;

    long long sum = 0, t = 1, prod = 1;

    for (int i = 0; i < n; i++) {
        int a;
        cin >> a;
        prod = prod * k % p;
        sum = (sum * a + t * prod) % p;
        t = t * a % p;
    }
    cout << sum * inverse((int)t, p) % p << '\n';
}

继续常数优化

上面的程序之所以慢,主要是输入量太大,其次是其中有许多模运算,而模运算较慢。我们来介绍一种快速取模的方法。

引理 设 。若 满足 ,则

证明:用带余除法把 表为 ,其中 。而 可表为 ,其中 。代入 ,可得

易见

注意,右半边只有在 时才可能取等号。

回到原问题。以 , 代入上面的引理,对于 ,有

令 ,显然 ,我们还有

所以 或 就是 除以 的余数。

模数 固定, 也固定。除以 向下取整可以用向右移位运算实现。这样,无需做除法就能算出 除以 的余数。

int p;
unsigned long long q; // q = -1ull / p + 1; 

int mod(long long x) {
    int r = x - ((__int128) x * q >> 64) * p; 
    if (r < 0) r += p;
    return r;
}
int p;
unsigned long long q;
int mod(long long x) {
    int r = x - ((__int128) x * q >> 64) * p; 
    if (r < 0) r += p;
    return r;
}

int main() {
    ios::sync_with_stdio(0);
    cin.tie(0);

    int n, k; cin >> n >> p >> k;
    q = -1ull / p + 1; // -1ull是2的64次方减1

    long long sum = 0, t = 1, prod = 1;

    for (int i = 0; i < n; i++) {
        int a; cin >> a;
        prod = mod(prod * k);
        sum = mod(sum * a + t * prod);
        t = t * a % p;
    }
    
    cout << sum * inverse((int)t, p) % p << '\n';
}

注意:我们常用 $x \bmod N$ 表示 $x$ 除以 $N$ 的余数。有的书上也用 $x\bmod N$ 表示含有 $x$ 的 mod $N$ 同余类。不难看出,二者是一回事。

Note: 操练同余式。