漫谈剩余系理论 & 欧拉定理 & 扩展欧拉定理

· · 算法·理论

欧拉函数

欧拉函数定义为:\varphi(n) 表示 1 \sim n 中所有与 n 互质的数的个数。

关于欧拉函数有下面的性质和用途:

int get_phi(int n) {
    phi[1] = 1;
    for (int i = 2; i <= n; i ++ ) {
        if (!st[i]) p[ ++ cnt] = i, phi[i] = i - 1;
        for (int j = 1; j <= cnt and 1ll * i * p[j] <= n; j ++ ) {
            st[i * p[j]] = true; if (i % p[j] == 0) {
                phi[i * p[j]] = phi[i] * p[j]; break;
            } phi[i * p[j]] = phi[i] * phi[p[j]];
        }
    }
}

完全剩余系

对于整数集 S = \{r_1, r_2 \cdots r_n\} 满足:

例如,对于 m = 5S = \{0, 1, 2, 3, 4\} 就是 5 的一个完全剩余系。通常地,将模 m 的完全剩余系记做 \mathbb{Z_m}

实际应用中,通常用 \mathbb{Z_m} = \{0, 1, 2 \cdots m - 1\} 来表示,也就是模 m 的最小非负完全剩余系。

推论 1:任意 m 个连续整数构成模 m 的完全剩余系。

推论 2:若 a \bot m\mathbb{Z_m} 是一个完全剩余系,则 S = \{a, 2a \cdots ma\} 构成一个完全剩余系。

下面证明推论 2

设集合 S = \{ra | r \in \mathbb{Z_m}\}。对于 r_i, r_j \in \mathbb{Z_m}, r_i \ne r_j。假设存在同余 r_i a \equiv r_j a (\bmod \ m)。由于 a \bot m,因此 r_i \equiv r_j(\bmod \ m)。根据完全剩余系的互异性,假设不成立。S 的互异性证明完成。

因为 S \subset \mathbb{Z},由定义 (2) 可得,\forall a \in S\mathbb{Z_m} 中有唯一的 r 满足 r \equiv a(\bmod \ m)。因此构成 S\mathbb{Z_m} 的单射。又因为 |S| = |\mathbb{Z_m}|,故构成 S\mathbb{Z_m} 的双射。

则对于任意整数,都存在 \mathbb{Z_m} 中的某个数 r 与之同余,亦存在 S 中某个数与之同余。定义 (1) 证明完成。因此 S 是一个完全剩余系。

简化剩余系

在完全剩余系基础上加上了更强的限制。

定义 \Phi_m = \{r_1, r_2 \cdots r_s\} 为模 m 的简化剩余系,当且仅当:

欧拉定理

定义:若 n, a 均为正整数,且 na 互质,则有 a ^ {\varphi(n)} \equiv 1(\bmod \ n)

证明:对于 n 的简化剩余系 \Phi_n,根据简化剩余系的推论 4 可知,S = \{ar | r \in \Phi_n\} 也构成模 n 的简化剩余系。

根据 \prod_{r \in \Phi_n} r \equiv \prod_{r \in S} r (\bmod \ n),有 a^{|\Phi_n|} = a ^ {\varphi(n)} \equiv 1 (\bmod \ n)。证毕。

由此可以得到更弱的定理 Farmet 小定理:\forall p \in \mathbb{P}a^{p - 1} \equiv 1 (\bmod \ p)

扩展欧拉定理

扩展欧拉定理的证明就不在我能力所及的范围内了。在这就放个结论吧。

a ^ b \equiv a ^ {b} (\bmod \ p), \ b < \varphi(p) \\\\ a ^ b \equiv a ^ {b \bmod \varphi(p) + \varphi(p) (\bmod \ p)}, \ b \ge \varphi(p) \end{matrix}\right.

这个柿子就比较有用了,可以用来搞降幂之类的事情。

欧拉降幂模板代码

例题

  1. P4139 上帝与集合的正确用法

2 ^ {2 ^ {2 ^ {\cdots ^ {2}}}} \bmod p 的值。

f(p) 表示 2 的幂塔对 p 取模的值。那么他就等于 2 ^ {f(\varphi(p)) + \varphi(p)}。由于 \varphi 函数下降速度极快(\log 速度),很快 \varphi(p) 降为 1。这个东西可以递归求解。

int solve(int p) {
    if (p == 1) return 0;
    int phi = get_phi(p);
    int s = solve(phi);
    return qpow(2, s + phi, p);
}
  1. CF906D Power Tower

给定一个数列 w_1, w_2 \cdots w_n 和模数 p, 每次询问一个区间 [l,r],求 w_l ^ {w_{l + 1} ^ {w_{l + 2} ^ {{\cdots}^{w_r}}}} \bmod\ p 的值

考虑欧拉降幂。欧拉函数下降速度很快,O(\log p) 次就能降为 1。因此可能只需要计算到 w_{l + k},后面的就全模 1 了(当然模 1 就是 0)。这个 kO(\log p) 级别的。

上面讨论的都是幂次大于 \varphi(p) 的情况。如果幂次小于 \varphi(p) 呢?怎么提前判断掉呢?

如果数列的每一项都 \ge 2,那么可以直接暴力判断,因为 O(\log p) 个数乘起来就会达到 p。但是如果存在 1 呢?这个也好办,直接把 r 调到 l 后边的第一个 1 前面就行,这样保证了 [l, r] 中没有 1。同时,这个 1 对答案也没有影响,因为 x ^ 1 = x, 1 ^ x = 1

下面是一份可供参考的代码实现:

#include <iostream>
#include <cstring>
#include <cstdio>
#include <map>
#define int long long

using namespace std;

const int N = 100010;
int n, m, p, a[N], suf[N];
map<int, int> bin;
int qpow(int a, int b, int p) {
    int s = 1; for (; b; b >>= 1, a = a * a % p)
        if (b & 1) s = s * a % p; return s;
}
bool check(int a, int b, int p) {
    int s = 1;
    for (; b; b >>= 1, a = a * a) {
        if (a >= p) return 0;
        if (b & 1) {
            s = s * a; if (s >= p) return 0;
        }
    } return 1;
}
int get_phi(int n) {
    int s = n; 
    for (int i = 2; i <= n / i; i ++ ) if (!(n % i))  {
        s = s / i * (i - 1); while (!(n % i)) n /= i;
    } if (n != 1) s = s / n * (n - 1); return s;
}
int solve(int u, int r, int p) {
    if (u == r) return a[u] % p;
    if (p == 1) return 0;
    int phi = bin[p], s = a[r]; bool flg = 0;
    for (int i = r; i > u + 1; i -- ) {
        if (!check(a[i - 1], s, phi)) { flg = 1; break; }
        s = qpow(a[i - 1], s, phi);
    } if (s >= phi) flg = 1; int pw;
    if (flg) pw = solve(u + 1, r, phi) + phi;
    else pw = s;
    return qpow(a[u], pw, p);
}
signed main() {
    scanf("%lld%lld", &n, &p);
    for (int i = 1; i <= n; i ++ )
        scanf("%lld", &a[i]);
    scanf("%lld", &m); int t = p; bin[1] = 1;
    while (t != 1) bin[t] = get_phi(t), t = bin[t];
    suf[n + 1] = n + 1;
    for (int i = n; i >= 1; i -- )
        if (a[i] == 1) suf[i] = i;
        else suf[i] = suf[i + 1];
    while (m -- ) {
        int l, r; scanf("%lld%lld", &l, &r);
        r = min(r, suf[l]); printf("%lld\n", solve(l, r, p));
    } return 0;
}