基础数论学习笔记

Tarantula

2022-01-28 14:21:05

Personal

[也许更好的阅读体验](https://www.cnblogs.com/ConanKID/p/number-theory.html) --- ## Part 1 扩展欧几里得(EXGCD) 裴蜀定理:关于 $x,y$ 的方程 $ax+by=c$ 有整数解,当且仅当 $\gcd(a,b)|c$。 证明略。 现在的问题是如何求出 $x,y$。 因为 $c$ 是 $\gcd(a,b)$ 的倍数,所以只需要求出 $ax+by=\gcd(a,b)$ 的解即可。 考虑使用归纳法。假设已经求出了两个数 $x_0,y_0$,使得 $b\cdot x_0+(a\bmod b)\cdot y_0=\gcd(a,b)$,我们尝试把 $x,y$ 用 $x_0,y_0$ 表示出来: $\kern{110pts}\gcd(a,b)=b\cdot x_0+(a\bmod b)\cdot y_0$ $\kern{150pts}=b\cdot x_0+(a-\lfloor\frac{a}{b}\rfloor\cdot b)\cdot y_0$ $\kern{150pts}=a\cdot y_0+b\cdot(x_0-\lfloor\frac{a}{b}\rfloor\cdot y_0)$ 因此可令 $x=y_0,y=x_0-\lfloor\frac{a}{b}\rfloor\cdot y_0$,即得到了原方程的一组解。 $x_0,y_0$ 也递归去求就好了。 发现 $a,b$ 在递归过程中的变化方式就是一个辗转相除的过程。这也是这个算法的得名原因。 ```cpp int exgcd(int a, int b, int &x, int &y) { if (b == 0) {x = 1, y = 0; return a;} int d = exgcd(b, a % b, y, x); y -= a / b * x; return d; } ``` P.S.:这个函数的停止递归条件里,由于 $y$ 的系数为 $0$,所以无论把它设什么值都是对的。只是如果设 $114514$ 这种值有爆 $\operatorname{long\kern{4pts}long}$的风险。 扩展欧几里得算法可以用来解同余方程,即求出最小的正整数 $x$,使得 $ax\equiv b\pmod m$。易知这个同余式成立的充要条件是存在正整数 $y$,使得 $ax+my=b$。因此只需要用扩欧求 $x$ 就行了。 接下来看 [这道题](https://www.luogu.com.cn/problem/P1516)。 设两只青蛙碰面的时间为 $t$ 天,那么显然: $$x+tm\equiv y+tn\pmod L$$ 所以 $$t(m-n)\equiv y-x\pmod L$$ 这个东西就是一个标准的同余方程形式了,可以用扩欧搞。需要注意的一点是得保证未知数的系数非负,即如果 $m<n$ 那么需要将方程两边同时取负。 ```cpp #include<bits/stdc++.h> #define int long long using namespace std; int m, n, a, b, l; int x, y; int exgcd(int a, int b, int &x, int &y) { if (b == 0) {x = 1, y = 0; return a;} int d = exgcd(b, a % b, y, x); y -= a / b * x; return d; } signed main() { cin >> a >> b >> m >> n >> l; if (m < n) swap(m, n), swap(a, b); int d = exgcd(m - n, l, x, y); if ((b - a) % d != 0) {cout << "Impossible" << endl; return 0;} x *= (b - a) / d, y *= (b - a) / d; l /= d; cout << (x % l + l) % l << endl; return 0; } ``` --- ## Part 2 乘法逆元 定义:若 $ax\equiv 1\pmod b$,则称 $x$ 为 $a$ 在模 $b$ 意义下的乘法逆元,记作 $a^{-1}=x$。 求法: 1. 扩展欧几里得。上文已经提到过,不再赘述。 2. 费马小定理。若 $p$ 为质数且 $\gcd(a,p)=1$,则 $a^{p-1}\equiv1\pmod p$,因此 $a^{-1}=a^{p-2}$。 3. 线性递推,详见 [P3811](https://www.luogu.com.cn/problem/P3811)。我们让模数 $m$ 对 $a$ 做带余除法,设 $m=aq+r$,那么 $$aq\equiv -r\pmod m$$ 因此 $$a\equiv -r\cdot q^{-1}\pmod m$$ 两边同取逆元 $$a^{-1}\equiv -r^{-1}\cdot q\pmod m$$ 于是我们就得到了递推公式: $$a^{-1}=(-(m\bmod a)^{-1}\cdot\lfloor\frac{m}{a}\rfloor)\bmod m $$ 我们知道除法运算在模意义下是不封闭的,因此在求 $\dfrac{a}{b}\bmod p$ 时并没有一个好的办法,但乘法逆元的引入解决了这一问题:我们可以将除以 $b$ 转化为乘上 $b$ 的逆元。于是解决一些题目变得方便了许多。 考虑 [这道题](https://www.luogu.com.cn/problem/P1593)。 首先将 $a$ 分解质因数,设 $a=\prod_{i=1}^n {c_i}^{p_i}$,那么 $a^b=\prod_{i=1}^n {c_i}^{b\cdot p_i}$。根据因数和定理,答案即为 $$\prod\limits_{i=1}^n (\sum\limits_{j=0}^{b\cdot p_i} {c_i}^{j})$$ 再用一下等比数列求和,式子化为 $$\prod\limits_{i=1}^n\dfrac{{c_i}^{b\cdot p_i+1}-1}{c_i-1}$$ 我们需要求这个东西对于 $9901$ 取模的结果。考虑用逆元将除以 $c_i-1$ 转化为乘上它的逆元,注意到 $9901$ 是个质数,所以直接用费马搞即可。 一个坑点是要特判 $c_i-1$ 不存在逆元的情况,即 $c_i\equiv 1\pmod {9901}$。此时显然 $\sum_{j=0}^{b\cdot p_i} {c_i}^{j}\equiv b\cdot p_i+1\pmod {9901}$。 于是就做完了。 ```cpp #include<bits/stdc++.h> #define int long long using namespace std; int a, b; int p[1005], c[1005], cnt; int ans = 1; int power(int a, int b, int p) { int res = 1; while (b) { if (b & 1) res = res * a % p; a = a * a % p; b >>= 1; } return res % p; } int calc(int k) { if (p[k] == 9901) return 1; if (p[k] % 9901 == 1) return (c[k] + 1) % 9901; int ans = (power(p[k], b * c[k] + 1, 9901) - 1) * power(p[k] - 1, 9899, 9901) % 9901; return (ans + 9901) % 9901; } signed main() { cin >> a >> b; int x = 2; while (a != 1) { if (a % x == 0) { p[++cnt] = x; while (a % x == 0) c[cnt]++, a /= x; } x++; } for (int i = 1; i <= cnt; i++) ans = ans * calc(i) % 9901; cout << ans << endl; return 0; } ``` --- ## Part 3 中国剩余定理(CRT) 中国剩余定理的基本形式如下: 现有 $n$ 个关于 $x$ 的方程,形如 $x\equiv a_i\pmod{m_i}$,且所有的 $m$ 两两互质,令 $M=m_1m_2\cdots m_n$,则 $x$ 在模 $M$ 意义下有唯一解。 扩展中国剩余定理(EXCRT),在原定理的基础上去除了所有的 $m$ 都互质这一条件,需要我们求出方程的一个解 $x$。 考虑归纳法。设前 $k-1$ 个方程的一个特解为 $x$,通解为 $x+aM$,其中 $M=\operatorname{lcm}(m_1,m_2\cdots m_{k-1})$,现在需要求一个整数 $t$,使得 $x+tM\equiv a_k\pmod {m_k}$。显然这个东西可以用 EXGCD 搞。 然后就没了。 以下是 [P4777](https://www.luogu.com.cn/problem/P4777) 的代码。 ```cpp #include<bits/stdc++.h> #define lcm nmsl #define int __int128 using namespace std; int n; int a[100005], m[100005]; int pm = 1; int ans, lcm; short tnnd[15], len; int read() { int x = 0, f = 1; char c = getchar(); while (!isdigit(c)) {if (c == '-') f = -1; c = getchar();} while (isdigit(c)) {x = (x << 3) + (x << 1) + (c ^ 48); c = getchar();} return x * f; } void print(int x) { len = 0; while (x) tnnd[++len] = x % 10, x /= 10; for (int i = len; i >= 1; i--) cout << tnnd[i]; cout << endl; } int exgcd(int a, int b, int &x, int &y) { if (b == 0) {x = 1, y = 0; return a;} int d = exgcd(b, a % b, y, x); y -= a / b * x; return d; } void get(int k) { int x, y; a[k] = (a[k] - ans % m[k] + m[k]) % m[k]; int d = exgcd(lcm, m[k], x, y); ans += x * a[k] / d % m[k] * lcm; lcm = lcm / d * m[k]; ans = (ans % lcm + lcm) % lcm; } signed main() { n = read(); for (int i = 1; i <= n; i++) m[i] = read(), a[i] = read(); ans = a[1], lcm = m[1]; for (int i = 2; i <= n; i++) get(i); print((ans % lcm + lcm) % lcm); return 0; } ``` 由于中间运算的结果可能会爆 $\operatorname{long\kern{4pts}long}$,所以可以考虑使用快速乘。我比较懒直接用了 int128。 --- ## Part 4 欧拉函数及扩展欧拉定理 定义:对于正整数 $n$,它的欧拉函数为不超过 $n$ 且与其互质的正整数数量,记为 $\varphi(n)$。 考虑如何计算 $\varphi(n)$。将 $n$ 分解质因数:$n=\prod_{i=1}^m {c_i}^{p_i}$,那么显然有 $$\varphi(n)=n\cdot \prod\limits_{i=1}^m\dfrac{c_i-1}{c_i}$$ 网上证明一大堆这里就不写了反正也不太重要。 这个式子就是欧拉公式。这样我们求 $\varphi(n)$ 只需要将其分解质因数即可,可以做到 $O(\sqrt n)$ 的复杂度。 另外如果要求 $1-n$ 中所有数的欧拉函数,可以考虑在筛质数的过程中顺便处理一下。时间复杂度 $O(n\log\log n)$ 或 $O(n)$,依实现方式而定。 欧拉定理:若 $\gcd(a,n)=1$,则 $a^{\varphi(n)}\equiv 1\pmod n$。 证明可以用既约剩余系。这里不写了。 扩展欧拉定理:若 $b\ge \varphi(n)$,那么 $a^b\equiv a^{b\bmod \varphi(n)+\varphi(n)}\pmod n$。证明略。 这个东西其实是相当有用的。注意到当 $b$ 特别大时,求 $a^b\bmod n$ 非常困难,以至于不仅要写高精,而且快速幂还会 T。但有了扩展欧拉定理我们就可以让 $b$ 先对 $\varphi(n)$ 取模再去求快速幂,高精不用写并且稳稳地不会超时。 以下是 [P5091](https://www.luogu.com.cn/problem/P5091) 的代码。 ```cpp #include<bits/stdc++.h> #define int long long using namespace std; int a, b, p; int phi; int getphi(int n) { int ans = n; for (int i = 2; i * i <= n; i++) if (n % i == 0) { ans = ans / i * (i - 1); while (n % i == 0) n /= i; } if (n > 1) ans = ans / n * (n - 1); return ans; } int power(int a, int b, int p) { int res = 1; while (b) { if (b & 1) res = res * a % p; a = a * a % p; b >>= 1; } return res % p; } signed main() { cin >> a >> p; phi = getphi(p); int x = 0, f = 0; char c = getchar(); while (!isdigit(c)) c = getchar(); while (isdigit(c)) { x = (x << 3) + (x << 1) + (c ^ 48); c = getchar(); if (x >= phi) x %= phi, f = 1; } b = x + f * phi; cout << power(a, b, p) << endl; return 0; } ``` 至此我们对所有基本运算(加减乘除以及乘方)的取模都有了一个较好的处理方式。 ~~所以接下来的数论肯定也会恶心起来。~~ --- 一些题目: - [P1082 [NOIP2012 提高组] 同余方程](https://www.luogu.com.cn/problem/P1082) - [P5656 【模板】二元一次不定方程 (exgcd)](https://www.luogu.com.cn/problem/P5656) - [P3811 【模板】乘法逆元](https://www.luogu.com.cn/problem/P3811) - [P4942 小凯的数字](https://www.luogu.com.cn/problem/P4942) - [P2054 [AHOI2005]洗牌](https://www.luogu.com.cn/problem/P2054) - [P4139 上帝与集合的正确用法](https://www.luogu.com.cn/problem/P4139)