数论初识
更好的阅读体验
本篇的用意在 恶补 认识 OI 中数论的基础部分。
一、约数、倍数与质合数
前置芝士
- 带余除法:
a \div b = c \cdots\cdots r \Leftrightarrow a \bmod b = r - 约数与倍数:
a \bmod b = 0 \Leftrightarrow b 是a 的约数\Leftrightarrow a 是b 的倍数 - 最大公因数:最大的数使得
c 同时为a ,b 的约数,记为\gcd(a, b) 或(a, b) - 最小公倍数:最小的数使得
c 同时为a ,b 的倍数,记为\operatorname{lcm}(a, b) 或[a, b] - 质数:
p 是质数当且仅当p 只有1 和p 两个约数(1 不是质数) - 整除:a 是
b 的约数\Leftrightarrow a\mid b - 唯一分解定理:质因数分解方案唯一
- 质数无限
-
-
- 第
n 个质数是O(n \log n) 级别的 -
\sum_{i=1}^n \frac{1}{i} = O(\log n) -
-
a\mid c \land b\mid c \land (a, b) = 1\Rightarrow ab\mid c -
a\mid bc \land (a, b) = 1 \Rightarrow a\mid c -
-
ab = (a, b) \cdot [a, b]
附上一个黑科技:
这份代码可以计算 long long。
LL mul(LL a, LL b, LL mod) { return (a * b - (LL)((long double)a / mod * b + 0.5) * mod + mod) % mod; }
计算 gcd
欧几里得算法(辗转相除法)
证明
简单来说: 因为 a,
严谨证明:
令
令
因为
拓展欧几里得(exgcd)
思路
解
(可以将
原式可化为
上式有解则表明
即
也即
那么
递归时求得
边界条件?
当
代码
// 计算 ax + by = 1 的解,并返回 gcd(a, b)
int exgcd(int a, int b, int &x, int &y) {
if(b == 0) { x = 1, y = 0; return a; }
int g = exgcd(b, a % b, x, y);
int tmp = x;
x = y;
y = tmp - a / b * y;
return g;
}
例题
洛谷 P1082
// https://www.luogu.com.cn/problem/P1082
#include <cstdio>
const int N = 5e6 + 5;
typedef long long LL;
// ax + by = 1
LL exgcd(LL a, LL b, LL &x, LL &y) {
if(b == 0) {
x = 1, y = 0;
return a;
}
LL g = exgcd(b, a % b, x, y);
LL tmp = x;
x = y;
y = tmp - a / b * y;
return g;
}
int main() {
LL a, b;
std::scanf("%lld%lld", &a, &b);
LL x, y;
exgcd(a, b, x, y);
while(x < 0) x += b;
x %= b;
std::printf("%lld\n", x);
return 0;
}
应用
求逆元。(逆元见下方↓)
可以用拓展欧几里得来推,解出的
二、同余问题
前置芝士
-
a \equiv b \pmod m \Leftrightarrow m\mid (b - a) -
a \equiv b \pmod m \land a \equiv b \pmod n \Rightarrow a \equiv b \pmod{[m, n]} -
(k, m) = d \land ka \equiv kb \pmod m \Rightarrow a \equiv b \pmod{\frac md}
逆元
如果有
令
欧拉定理
欧拉函数
所有的
n 的简化剩余系的个数记为
欧拉定理
结论
证明
令
则
(因为
所以
应用
如果
线性求逆元
思路
令
则
即 inv[i] = (-(p / i) * inv[p % i] + p) % p,
也即 inv[i] = (p - p / i) * inv[p % i] % p 。
代码
{% note no-icon info 代码 %}
// 筛出 n 以内的逆元
void get_inv() {
inv[1] = 1;
for(int i = 2; i <= n; i++) inv[i] = (long long)(p - p / i) * inv[p % i] % p;
}
{% endnote %}
例题
洛谷 P3811
// https://www.luogu.com.cn/problem/P3811
#include <cstdio>
const int N = 20000528 + 5;
int inv[N];
int main() {
int n, p;
scanf("%d%d", &n, &p);
inv[1] = 1;
for(int i = 2; i <= n; i++) inv[i] = (long long)(p - p / i) * inv[p % i] % p;
for(int i = 1; i <= n; i++) printf("%d\n", inv[i]);
return 0;
}
中国剩余定理(CRT)
引入
先来看一个例子:
求最小的正整数 x,满足
x \equiv 2 \pmod 3 ,x \equiv 3 \pmod 5 ,x \equiv 2 \pmod 7 。
解法:令
原理
要想让
那么如何求这样的
以
即把
最后算出
结论
若有方程:
其中
令
则方程的解为
代码
// 有两个数组:r[] 和 p[],分别表示余数和模数
// 一共有 n 组方程
int crt() {
int P = 1;
for(int i = 1; i <= n; i++) P *= p[i];
int x = 0;
for(int i = 1; i <= n; i++) {
int ret = 1;
ret = ret * (P / p[i]) % P;
int inv, tmp;
exgcd(P / p[i], p[i], inv, tmp);
while(inv <= 0) inv += p[i];
inv %= p[i];
ret = ret * inv % P;
ret = ret * r[i] % P;
x = (x + ret) % P;
}
return x;
}
例题
洛谷 P1495
// https://www.luogu.com.cn/problem/P1495
#include <cstdio>
const int N = 1e5 + 5;
typedef long long LL;
LL r[N], p[N];
int n;
LL exgcd(LL a, LL b, LL &x, LL &y) {
if(b == 0) { x = 1, y = 0; return a; }
LL g = exgcd(b, a % b, x, y);
LL tmp = x;
x = y;
y = tmp - a / b * y;
return g;
}
LL crt() {
LL P = 1;
for(int i = 1; i <= n; i++) P *= p[i];
LL x = 0;
for(int i = 1; i <= n; i++) {
LL ret = 1;
ret = ret * (P / p[i]) % P;
LL inv, tmp;
exgcd(P / p[i], p[i], inv, tmp);
while(inv <= 0) inv += p[i];
inv %= p[i];
ret = ret * inv % P;
ret = ret * r[i] % P;
x = (x + ret) % P;
}
return x;
}
int main() {
std::scanf("%d", &n);
for(int i = 1; i <= n; i++) std::scanf("%lld%lld", &p[i], &r[i]);
std::printf("%lld\n", crt());
return 0;
}
拓展中国剩余定理(exCRT)
与 CRT 不同,这次我们尝试两两合并方程。
考虑两个方程
我们将它们转成不定方程:
对于这个方程,我们用拓展欧几里得算出
那么这两组方程的解为
代码
// 有两个数组:r[] 和 p[],分别表示余数和模数
// 一共有 n 组方程
LL excrt() {
LL a1, b1, a2, b2, x, y;
a1 = r[1], b1 = p[1];
for(int i = 2; i <= n; i++) {
a2 = r[i], b2 = p[i];
if(a2 < a1) std::swap(a1, a2), std::swap(b1, b2);
LL a = b1, b = b2, c = a2 - a1;
LL g = exgcd(a, b, x, y);
if(c % g) return -1;
a /= g, b /= g, c /= g;
b1 = lcm(b1, b2);
y = y * (b1 - c) % b1;
a1 = (a2 + b2 * y % b1) % b1;
}
return a1;
}
例题
洛谷 P4777
注意此题需要用前面的乘法技巧,因为可能会爆 long long。
#include <cstdio>
#include <algorithm>
typedef long long LL;
const int N = 1e5 + 5;
LL r[N], p[N];
int n;
LL gcd(LL x, LL y) { return y == 0 ? x : gcd(y, x % y); }
LL lcm(LL x, LL y) { return x / gcd(x, y) * y; }
LL exgcd(LL a, LL b, LL &x, LL &y) {
if(b == 0) { x = 1, y = 0; return a; }
LL x_, y_;
LL g = exgcd(b, a % b, x_, y_);
x = y_;
y = x_ - a / b * y_;
return g;
}
LL mul(LL a, LL b, LL mod) { return (a * b - (LL)((long double)a / mod * b + 0.5) * mod + mod) % mod; }
// 有两个数组:r[] 和 p[],分别表示余数和模数
// 一共有 n 组方程
LL excrt() {
LL a1, b1, a2, b2, x, y;
a1 = r[1], b1 = p[1];
for(int i = 2; i <= n; i++) {
a2 = r[i], b2 = p[i];
if(a2 < a1) std::swap(a1, a2), std::swap(b1, b2);
LL a = b1, b = b2, c = a2 - a1;
LL g = exgcd(a, b, x, y);
if(c % g) return -1;
a /= g, b /= g, c /= g;
b1 = lcm(b1, b2);
y = mul(y, b1 - c, b1);
a1 = (a2 + mul(b2, y, b1)) % b1;
}
return a1;
}
int main() {
scanf("%d", &n);
for(int i = 1; i <= n; i++) scanf("%lld%lld", &p[i], &r[i]);
printf("%lld\n", excrt());
return 0;
} /*
3
11 6
25 9
33 17
*/
小结
数论初步到这里就差不多了,看到这里说明你已经入门了。
后面的就等到 数论进阶 来讲吧。
完结撒花~