数论
模运算
取模操作有三个性质(模数为 p
):
加:(a + b) % p = (a % p + b % p) % p
。
减:(a - b + p) % p = (a % p - b % p + p) % p
,因为我们不知道 a % p
和 b % p
的大小关系,可能减出负数,所以需要加上一个 p
再取模。
乘:(a * b) % p = (a % p) * (b % p) % p
。
但是,需要注意,取模操作是不能对除法进行类似运算的,需要用逆元,下文会讲到。
快速幂
对于幂运算 \(a ^ n\),如果我们暴力的一个一个乘,时间复杂度是 \(O(n)\) 的,当 \(n\) 足够大时,会超时。
所以,我们需要快速幂来解决这个问题。
有两种解法,一种是分治,另一种是利用位运算实现快速幂。
分治
首先,我们可以想到,当 \(n\) 为偶数时,\(a ^ n = a ^ {\frac{n}{2}} \times a ^ {\frac{n}{2}}\) 的,也就是说,我们只要求出 \(a ^ {\frac{n}{2}}\) 就行了。
当 \(n\) 为奇数时,\(a ^ n = a ^ {\lfloor \frac{n}{2} \rfloor} \times a ^ {\lfloor \frac{n}{2} \rfloor} \times a\),所以我们需要求出 \(a ^ {\lfloor \frac{n}{2} \rfloor}\)。
所以,代码如下:
using ll = long long;
ll P(int a, int b) {
if (!b) {
return 1;
}
ll tmp = P(a, b / 2);
return tmp * tmp % mod * (b % 2 ? a : 1) % mod;
}
位运算
假设我们要求 \(a ^ {10}\),我们先把 \(10\) 转成二进制,也就是 \(1010\)。
我们不难发现,\(a ^ {10} = a ^ 8 \times a ^ 2\),而刚好,\(10\) 的二进制表示的两个 \(1\) 所对应的位权刚好也是 \(8, 2\)。
所以,代码如下:
using ll = long long;
ll P(int a, int b) {
ll ret = 1;
while (b) {
if (b & 1) { // b 的最后一位是 1
ret = ret * a % mod;
}
a = a * a % mod, b >>= 1;
}
return ret;
}
两种实现的时间复杂度都是 \(O(\log b)\) 的,但是分治的做法会稍微慢一点点(常数更大),递归是需要一些时间的。
GCD 和 LCM
最大公约数(GCD)和最小公倍数(LCM),研究整除的性质。
整除:
若 \(a\) 可以整除 \(b\),记为 \(a \mid b\),其中 \(a, b\) 均为整数,\(a \neq 0\),\(b\) 是 \(a\) 的倍数,\(a\) 是 \(b\) 的因子。
性质:
-
若 \(a, b, c\) 均为整数,且 \(a \mid b, b \mid c\),则 \(a \mid c\)。
-
若 \(a, b, m, n\) 均为整数,且 \(c \mid a, c\mid b\),则 \(c \mid (m \times a + n \times b)\)。
-
定理:带余除法。如果 \(a, b\) 均为整数且 \(b > 0\),则存在唯一的整数 \(1, r\),使得 \(a = b \times q + r\),\(0 \le r < b\)。
GCD
gcd(a, b)
指的是 \(a, b\) 的最大公因数,也就是最大的可以同时整除 \(a, b\) 的数。
注意:gcd(a, b) = gcd(|a|, |b|)
。
欧几里得算法
辗转相除法,时间复杂度为 \(O(\log b)\)。
int gcd(int a, int b) {
return !b ? a : gcd(b, a % b);
}
LCM
lcm(a, b)
指的是 \(a, b\) 的最小公倍数,从算术基本定理得到。
算术基本定理
任意一个大于 \(1\) 的正整数 \(n\) 都可以分解成有限个素数的乘积:\(n = p_1 ^ {a_1} \times p_2 ^ {a_2} \times \dots \times p_k ^ {a _ k}\),其中 \(a_i\) 都是正整数,\(p_i\) 都是素数且从小到大。
设 \(x = p_1 ^ {a_1} \times p_2 ^ {a_2} \times \dots \times p_k ^ {a_k}, y = p_1 ^ {b_1} \times p_2 ^ {b_2} \times \dots \times p_k ^ {b_k}\)。
那么 \(\gcd (a, b) = p_1 ^ {\min(a_1, b_1)} \times p_2 ^ {\min(a_2, b_2)} \times \dots \times p_k ^ {\min(a_k, b_k)}, lcm(a, b) = p_1 ^ {\max(a_1, b_1)} \times p_2 ^ {\max(a_2, b_2)} \times \dots \times p_k ^ {\max(a_k, b_k)}\)
可以推出 \(lcm(a, b) = a \times b \div \gcd(a, b)\)。
注意:在计算 \(lcm\) 时,写 \(lcm(a, b) = a \div \gcd(a, b) \times b\),如果先乘再除,可能会溢出。
所以,代码如下:
int gcd(int a, int b) {
return !b ? a : gcd(b, a % b);
}
int lcm(int a, int b) {
return a / gcd(a, b) * b;
}
裴蜀定理
一个关于 GCD 的定理。
裴蜀定理:如果 \(a, b\) 均为整数,则有整数 \(x, y\) 使得 \(ax + by = \gcd(a, b)\)。
推论:整数 \(a, b\) 互质当且仅当存在整数 \(x, y\),使得 \(ax + by = 1\)。
理解:对于任意 \(x, y\),\(d = ax + by\),\(d\) 肯定是 \(\gcd(a, b)\) 的整数倍。
洛谷 P4549
我们先看一对数的情况:\(A_1 X_1 = A_2 X_2\),把它改成 \(ax + by\) 的形式。
根据裴蜀定理,有整数 \(x, y\) 使得 \(ax + by = \gcd(a, b)\),也就是说,\(ax + by\) 的最小值就是 \(\lvert \gcd(a, b) \rvert\),所以 \(A_1 X_1 + A_2 X_2\) 的最小值就是 \(gcd(A_1, A_2)\)。
再继续合并 \(A_3, A_4, A_5, \dots , A_N\)。
代码如下:
#include <bits/stdc++.h>
using namespace std;
const int N = 25;
int n, a[N];
long long ans;
int gcd(int a, int b) {
return (!b ? a : gcd(b, a % b));
}
int main() {
ios::sync_with_stdio(0), cin.tie(0);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
for (int i = 1; i <= n; i++) {
ans = abs(gcd(ans, a[i])); // gcd(ans, a[i]) 可能是负数
}
cout << ans;
return 0;
}
同余
同余:设 \(m\) 是正整数,若 \(m \mid (a - b)\),则说明 \(a \equiv b \ \pmod m\)。
定理和性质
设 \(m\) 为正整数,模 \(m\) 的同余满足以下性质:
-
自反性:若 \(a\) 是整数,则 \(a \equiv a \pmod m\)。
-
对称性:若 \(a, b\) 均为整数,且 \(a \equiv b \pmod m\),则 \(b \equiv a \pmod m\)。
-
传递性:若 \(a, b, c\) 均为整数,且 \(a \equiv b \pmod m, b \equiv c \pmod m\),则 \(a \equiv c \pmod m\)。
若 \(a, b, c, m\) 均为整数,\(m > 0\),且 \(a \equiv b \pmod m, c \equiv d \pmod m\),则有以下性质:
-
加:\(a + c \equiv b + d \pmod m\)。
-
减:\(a - c \equiv b - d \pmod m\)。
-
乘:\(ac \equiv bd \pmod m\)。
-
同余的幂:若 \(k\) 为整数,且 \(k > 0\),则有 \(a ^ k \equiv b ^ k \pmod m\)。
逆
逆的概念
给定一个整数 \(a\),且 \(a\) 与 \(m\) 互质,则称 \(ax \equiv 1 \pmod m\) 的一个解为 \(a\) 在模 \(m\) 意义下的逆,记为 \(a ^ {-1}\)。
求逆
扩展欧几里得算法(求单个逆)
\(ax \equiv 1 \pmod m\),即 \(ax + my = 1\),先求出这个方程的一个特解 \(x_0\),通解是 \(x = x_0 + mn\)。然后计算最小整数解 \(((x_0 \mod m) + m) \mod m\)。
代码如下:
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
ll a, b;
void gcd(ll a, ll b, ll &x, ll &y) {
if (!b) {
x = 1, y = 0;
return ;
}
gcd(b, a % b, y, x);
y -= a / b * x;
}
ll Solve(ll a, ll b) {
ll x, y;
gcd(a, b, x, y);
return (x % b + b) % b;
}
int main() {
ios::sync_with_stdio(0), cin.tie(0);
cin >> a >> b;
cout << Solve(a, b);
return 0;
}
时间复杂度为 \(O(\log b)\)。
费马小定理(求单个逆)
若 \(p\) 为质数,且 \(p \nmid a\),有 \(a ^ {p - 1} \equiv 1 \pmod p\)
不给证明,当结论来记。
-
\(\frac{x}{a}\) 在模 \(p\) 意义下为 \(\frac{x}{a} \equiv \frac{x}{a} \cdot a ^ {p - 1} \equiv x \cdot a ^ {p - 2} \pmod p\)
-
\(a\) 在模 \(p\) 意义下的乘法逆元为 \(a ^ {p - 2}\)(\(p\) 为质数,\(p \nmid a\)),记作:\(a ^ {-1} \nmid a ^ {p - 2} \pmod p\)
设 \(p\) 为素数,\(a\) 是正整数且 \(a, p\) 互质,则有 \(a ^ {p - 1} \equiv 1 \pmod p\)。
\(a \times a ^ {n - 2} \equiv 1 \pmod p\),则 \(a ^ {p - 2}\) 就是 \(a\) 在模 \(p\) 意义下的逆,需要用快速幂求解。
using ll = long long;
ll P(int a, int b) {
if (!b) {
return 1;
}
ll tmp = P(a, b / 2);
return tmp * tmp % mod * (b % 2 ? a : 1) % mod;
}
ll F(int x) {
return P(x, mod - 2);
}
时间复杂度为 \(O(\log mod)\)
递推(求多个逆)
求出 \(1 \sim n\) 的所有逆,用递推,时间复杂度为 \(O(n)\)。
首先,\(i = 1\) 的逆是 \(1\),若 \(i > 1\),则这样求:
-
设 \(p \div i = k\),余数为 \(r\),则 \(k \times i + r \equiv 0 \pmod p\)。
-
在等式两边乘上 \(i ^ {-1} \times r ^ {-1}\),得到 \(k \times r ^ {-1} + i ^ {-1} \equiv 0 \pmod p\)。
-
移项可得 \(i ^ {-1} \equiv -k \times r ^ {-1} \pmod p\),即 \(i ^ {-1} \equiv -p \div i \times r ^ {-1} \pmod p\),也就是 \(i ^ {-1} \equiv (p - p \div i) \times r ^ {-1} \pmod p\)。
代码如下:
#include <bits/stdc++.h>
using namespace std;
const int N = 3e6 + 10;
int n, p;
long long inv[N];
int main() {
ios::sync_with_stdio(0), cin.tie(0);
cin >> n >> p, inv[1] = 1, cout << "1\n";
for (int i = 2; i <= n; i++) {
inv[i] = (p - p / i) * inv[p % i] % p;
cout << inv[i] << '\n';
}
return 0;
}
逆与除法取模
如果我们要求 \((a \div b) \mod m\),可以先将 \(a \div b\),再对 \(m\) 取模。
但是,如果 \(a, b\) 都是很大的数呢?比如说 \(2000!\),会溢出,导致计算错误。
所以,我们记 \(k = b ^ {-1}\),则有 \(b \times k \equiv 1 \pmod m\)。
则 \(a \div b \equiv a \times 1 \div b \equiv a \times (1 \div b) \equiv a \times k \pmod m\)。
所以,我们可以求出 \(b\) 的逆,再求 \((a \div b) \mod m\)。
素数(质数)
素数:只能被 \(1\) 和自己整除的正整数。
素数的分布:随着整数的增大,素数的分布越来越稀疏。
哥德巴赫猜想:每个大于 \(2\) 的正偶数可以写成两个素数的和。
试除法
我们应该如何判断 \(x\) 是不是一个质数呢?
由于约数是成对出现的,所以我们只需要枚举 \(1 \sim \sqrt{x}\) 中的所有 \(i\),判断 \(x\) 是否可以被 \(i\) 整除即可。
bool Is_Prime(int x) {
if (x == 2) return 1;
for (int i = 2; i * i <= x; i++) {
if (x % i == 0) return 0;
}
return 1;
}
素数筛
给定一个 \(n \ (1 \le n \le 10 ^ 6)\),请你求出 \(2 \sim n\) 的所有质数。
如果我们一个一个的判断是不是质数,时间复杂度就是 \(O(n \times \sqrt{n})\),最高会达到 \(10 ^ 9\) 级别,不够优秀。
我们需要快速的筛出所有的质数。
埃氏筛
埃氏筛的本质是利用了质数的定义,也就是说,对于一个质数 \(x\) 来说,\(2 \sim x - 1\) 都不是它的约数,那么,对于 \(2 \sim x - 1\) 中的所有数,它们的倍数中都没有 \(x\),所以,在遍历到某一个质数 \(i\) 时,我们给 \(1 \sim n\) 中所有的 \(i\) 的倍数都打上标记就可以了。
但是,其实,我们是不用从 \(2 \times i\) 开始枚举的,因为 \(2 \times i\) 已经被 \(2\) 筛过了,只需要从 \(i \times i\) 开始即可。
void Eratosthenes(int n) {
f[1] = 1;
for (int i = 2; i <= n; i++) {
if (!f[i]) {
prime[++c] = i;
for (int j = i * i; j <= n; j += i) {
f[j] = 1;
}
}
}
}
这样,时间复杂度就是 \(O(n \times \log \log n)\),接近 \(O(n)\),比较优秀。
欧拉筛
我们先来想一想,为什么埃氏筛有一个 \(\log \log n\) 呢?
因为每个数都会被筛很多次,像 \(12\),它会被 \(2, 3\) 各筛到一次。
所以,欧拉筛的原理就是让每个数都只被它的最小质因数筛一次。
也就是说,枚举到 \(i\) 时,我们给 \(i\) 和已经筛出的所有质数相乘所得到的数打上标记即可。
void Euler(int n) {
for (int i = 2; i <= n; i++) {
if (!f[i]) prime[++c] = i;
for (int j = 1; j <= c; j++) {
if (prime[j] * i > n) break;
f[prime[j] * i] = 1;
if (i % prime[j] == 0) break;
}
}
}
事实上,由于我们是用最小质因数来筛的每个数,所以可以顺便记录下最小质因数。
欧拉函数
欧拉函数的定义和性质
欧拉函数的定义:设 \(n\) 是一个正整数,欧拉函数 \(\phi(n)\) 定义为不超过 \(n\) 且与 \(n\) 互素的正整数的个数。
定理 1
设 \(p, e\) 是互质的正整数,那么 \(\phi(p \times q) = \phi(p) \times \phi(q)\)。
所以欧拉函数是一种积性函数,则有以下推论。
若 \(n = p_1 ^ {a_1} \times \dots \times p_m ^ {a_m}\),其中 \(p_1, p_2, \dots, p_m\) 互素,则 \(\phi(n) = \phi(p_1 ^ {a_1}) \times \dots \times \phi(p_m ^ {a_m})\)。
定理 2
设 \(n\) 为正整数,那么
这个定理说明了 \(n\) 与 \(\phi(n)\) 的关系:\(n\) 的正因数(包括 \(1\) 和自身)的欧拉函数之和等于 \(n\)。
求欧拉函数的通解公式
欧拉定理
设 \(m\) 是一个正整数,\(a\) 是一个整数且 \(a\) 与 \(m\) 互质,即 \(\gcd(a, m) = 1\),则有 \(a ^ {\phi(m)} \equiv 1 \pmod m\)。
定理 3
设 \(n = p_1 ^ {a_1} \times \dots \times p_m ^ {a_m}\) 为正整数 \(n\) 的质幂因数分解,那么
上述公式有以下两种特殊情况。
-
若 \(n\) 是质数,\(\phi(n) = n - 1\)。
-
若 \(n = p ^ k\),\(p\) 是质数,有 \(\phi(n) = \phi(p ^ k) = p ^ k - p ^ {k - 1} = p ^ {k - 1}(p - 1) = p ^ {k - 1} \times \phi(p)\)
所以,可以用 \(O(\sqrt{n})\) 的时间复杂度求出单个欧拉函数。
用线性筛(欧拉筛)求 \(1 \times n\) 内的所有欧拉函数
现在我们需要求 \(1 \sim n\) 的所有欧拉函数,如果我们一个一个的求的话,时间复杂度为 \(O(n \times \sqrt{n})\),效率不够高。
所以,我们需要优化。
首先,我们用线性筛求出 \(1 \sim n\) 所有数的最小质因数,再递推求解所有数的欧拉函数。总时间复杂度为 \(O(n)\)。
void Get_Phi() {
f[1] = 1;
for (int i = 2; i <= n; i++) {
if (!f[i]) prime[++c] = i, phi[i] = i - 1;
for (int j = 1; j <= c; j++) {
if (prime[j] * i > n) break;
f[prime[j] * i] = 1;
phi[prime[j] * i] = phi[i] * (i % prime[j] ? prime[j] - 1 : prime[j]);
if (i % prime[j] == 0) break;
}
}
}
Dilworth 定理
最长上升(下降)子序列长度 = 将序列划分为若干个不上升(不下降)子序列的最少序列个数。