数论
快速幂
快速幂
快速幂主要是能在 的时间复杂度求解 。其思路在于任何一个数都可以写成以下形式 。{} 序列是严格单调递增的。因此 可以写成 ,从而一步一步往下计算 ,最多计算 次即可。
核心代码如下 :
int kpow(int a, int k, int m) {
a %= m; int ans = 1 % m; while (k) { if (k & 1) ans = (LL) ans * a % m; k >>= 1; a = (LL) a * a % m; } return ans;}龟速乘
龟速乘主要是 的时间内计算 ,这种计算方法主要应用在 会爆掉 long long 的情况下,思路和快速幂一致,也是利用 的二进制拆分。
核心代码如下:
LL kmul(LL x, LL y, LL m) { LL ans = 0; while (y) { if (y & 1) ans = (ans + x) % m; x = (x + x) % m; y >>= 1; } return ans;}素数筛
首先来看 1-N 中有多少个素数,也就是 。当 时, ; 当 时,。了解在什么范围内有多少素数,有利于掌握相应的时间复杂度。
下面给出三种筛法(杜教筛以后学到再说)
- 埃式筛
时间复杂度为 。
核心代码如下:
int primes[N], cnt = 0;bool st[N];void get_primes(int n) {cnt = 0;memset(st, false, sizeof st);for (int i = 2; i <= n; i ++ ) {if (st[i]) continue;primes[cnt ++ ] = i;for (int j = i + i; j <= n; j += i)st[j] = true;}}
- 线性筛
时间复杂度为 。
核心思路在于利用每个数的最小质数将其筛掉
核心代码如下:
int primes[N], cnt = 0;bool st[N];void get_primes(int n) {cnt = 0;memset(st, 0, sizeof st);for (int i = 2; i <= n; i ++ ) {if (!st[i]) primes[cnt ++ ] = i;for (int j = 0; i <= n / primes[j]; j ++ ) {st[i * primes[j]] = true;if (i % primes[j] == 0) break;}}}
- 杜教筛 时间复杂度为 。
素数判定
试除法
时间复杂度: 当 时,足够判断 代码如下:
bool is_prime(int x) {
if (x == 1) return false; for (int i = 2; i <= x / i; i ++ ) if (x % i == 0) return false; return true;}另外,这个方法还可以加快速度,根据[[#唯一分解定理]],只需要使用 里面的素数来判断即可。 因此,这里有个策略是利用线性筛提前预处理好前 的所有素数,但由于时间和空间限制,这个 的范围最大是 代码如下:
bool is_prime(int x) {
if (x == 1) return false; for (int i = 0; prime[i] <= x / prime[i]; i ++ ) if (x % prime[i] == 0) return false; return true;}Miller-Rabin 算法
费马小定理
设 是素数, 是正整数且与 互素,那么有
费马小定理目前作用有二:1. 这个是判断素数的必要条件;2.同余求逆
对于应用一:要判断 是否为素数,可以随机 来判断 模 是否为 1,如果不为 1 ,一定不是素数,如果为 , 可能是素数。因为一定有一些数(Carmichael 数)满足该式子,但是不为素数。但是这种数出现的概率不大。
对于应用二:由于 ,因此 是在 与 互素下同余 的关于 的逆元。注意这个后缀很重要,如果不互素,得使用扩展欧几里得
二次探测定理
如果 是一个奇素数,且 ,则方程 仅有两个解: 和 。当 时,方程 仅有两个解 和 把 和 称为“ 对模 来说 的平凡平方根”
我们利用其逆否命题:对于方程 ,(注意 不一定是奇素数),如果对模 存在 的非平凡平方根,则 不可能是奇素数或者奇素数的幂
根据上面两个定理,可设计 Miller-Rabin 算法
- 如果 ,不是素数
- 如果 ,是素数
- 如果 ,不是素数
- 那么剩下的就是 ,且 是奇数,来判断其是否为素数
- 首先随机 ,判断 模 是否为 1,不是则说明不是素数
- 等于 1 的话利用二次探测定理继续判断,因此此时 是偶数,所以有方程 ,所以可以判断 是否为 或者 ,不是则说明不为素数,是则继续判断直到 为奇数
代码中对 4.2 的利用不太一样,是直接对 除到一个奇数,然后从这个奇数开始算到 ,如果都满足二次探测定理,最后则判断其是否满足费马小定理
只考虑 int 时间复杂度: ,出错的概率为
代码如下:
#include <random>random_device rd;mt19937 gen(rd());
int kpow(int a, int k, int m) {
a %= m; int ans = 1; while (k) { if (k & 1) ans = (LL) ans * a % m; k >>= 1; a = (LL) a * a % m; } return ans;}
bool witness(int a, int n) { int u = n - 1, t = 0; while ((u & 1) == 0) u >>= 1, t ++ ; int x1, x2; x1 = kpow(a, u, n); for (int i = 0; i < t; i ++ ) { x2 = kpow(x1, 2, n); if (x2 == 1 && x1 != 1 && x1 != n - 1) return true; x1 = x2; } if (x1 != 1) return true; return false;}
bool miller_rabin(int n, int s) {
if (n < 2) return false; if (n == 2) return true; if (n % 2 == 0) return false; // i < min(s, n),因为随机数取的是 [1,n-1],当 s > n 时没啥意义 for (int i = 0; i < s && i < n; i ++ ) { int a = gen() % (n - 1) + 1; if (witness(a, n)) return false; } return true;}考虑到 long long 的乘法会溢出的情况下需要使用龟速乘,时间复杂度为: 。
分解质因数
唯一分解定理
任何 的整数都可以分解成若干个质数乘积。也就是说
质因数分解
思路:首先利用一个数 的质因数中大于 的最多只有一个的特性,只需要枚举到 的情形就行,枚举的过程中, 可以通过枚举到的质因数逐渐变小,最后退出循环后,只要 仍旧大于 1,说明这是剩下来的那个质因数。 时间复杂度: 代码如下:
void divide(int x) { for (int i = 2; i <= x / i; i ++ ) { int cnt = 0; while (x % i == 0) cnt ++ , x /= i; if (cnt) cout << i << ' ' << cnt << endl; } if (x > 1) cout << x << ' ' << 1 << endl;}另外,当 的范围不是很大的时候,也就是 左右的时候,可以通过只枚举素数来加快速度。时间复杂度大概是
代码如下:
void divide(int n) {
for (int i = 0; primes[i] <= n / primes[i]; i ++ ) { int cnt = 0; while (n % primes[i] == 0) cnt ++ , n /= primes[i]; if (cnt > 0) { cout << primes[i] << ' ' << cnt << endl; } } if (n > 1) cout << n << ' ' << 1 << endl;}另外,如果要存的话,使用差不多 100 大小的数组就能存,因为对于 int 型的数质因数最多有十几个,一个因数的个数最多有三十几个
pollard_rho 启发式方法分解质因数
阶乘相关
勒让德定理
在正数 的质因数分解中,质数 的指数记作 ,则
阶乘质因数分解
思路就是唯一分解定理和勒让德定理的应用。 时间复杂度在 左右。 代码如下:
// 首先是找质数,根据上面的线性筛
// 然后就是勒让德计算for (int i = 0; i < cnt; i ++ ) { int cnt = 0; for (int k = n / primes[i]; k > 0; k /= primes[i]) cnt += k; cout << primes[i] << ' ' << cnt << endl; }威尔逊定理
若 为素数,则 可以整除 威尔逊定理四种表示方法:
- , 为正整数
- 核心记住第一点
约数
约数相关性质
-
约数个数 根据唯一分解定理,约数个数 那么前 个数的约数个数之和怎么算呢,可以根据第 个数在 中是多少个数的约数,也就是 个,因此 大概是这个量级。这对分析时间复杂度有帮助。
-
约数之和 一个数所有约数之和为 ,这个很容易证明,根据乘法分配法,最后加法分隔开的每个数,都是从每个括号里面选择一个数进行相乘,这样刚好就枚举了所有的约数可能。 代码如下:
// 第一步分解质因数int ans = 1;for (auto [u, v]: um) {int p = 1, t = 0;for (int i = 0; i <= v; i ++ ) t = (t + p) % MOD, p = 1ll * p * u % MOD;ans = 1ll * ans * t % MOD;} -
最多的约数个数 在
int范围内,约数个数最多的数大概有 1600 个约数。这对分析时间复杂度相当有帮助。 -
快速求出所有约数 首先第一步,对数先进行质因数分解,然后 dfs 求得所有约数,根据上述的最多约数个数为 1600 个左右,因此 dfs 的时间一般来说是小于质因数分解的速度的。因此,求所有约数的时间极限变成了如何快速求质因数分解。[[#质因数分解]] 其中,dfs 代码为
void dfs(int cur, int val) {if (cur >= idx) {res.push_back(val);return ;}for (int i = 0, t = 1; i <= c[cur]; i ++ , t *= p[cur])dfs(cur + 1, val * t);
} ```
最大公约数
欧几里得算法
时间复杂度: 最大公约数的公式 证明:设 为 的任意一个约数,那么 也为 的约数。显然, 是 的约数成立,由于 ,根据 。因此, 也是 的约数。那么如果 是 的最大公约数,则 也是 的最大公约数。 设 为 的任意一个约数,那么 也是 的约数。显然, 是 的约数成立,由于 。根据同余公式,则 也是 的约数。同理, 的最大公约数也是 的最大公约数。 综上:。
代码如下:
LL gcd(LL a, LL b) { return b ? gcd(b, a % b) : a;}- 拓展 根据此公式,序列的区间 等价于序列差分数组的
更相减损法
时间复杂度 因此一般使用 算法
stein 算法
如果 都是偶数 如果 其中一个为奇数,根据 如果 互质,因此此处 取 ,则有 ,这里适用高精度,但是建议py
裴蜀定理
如果 和 均为整数,则有整数 和 使
因此,有推论:当 互素时,有整数 和 使得
线性丢番图方程
根据[[#裴蜀定理]] ,方程 有整数解的充要条件是 是 的倍数。假设 是一组特解,所有通解可以表示为 , 为任意整数
扩展欧几里得求二元线性丢番图的特解
首先利用扩展欧几里得求方程 的特解,代码如下
LL exgcd(LL a, LL b, LL &x, LL &y) { if (b == 0) { x = 1, y = 0; return a; } LL d = exgcd(b, a % b, y, x); y -= a / b * x; return d;}那么可以根据这个来求 的特解
- 判断 是否整数
- 求 的特解
- 在 两边乘 ,可得特解为
- 通解为 ,其中
同余
称为 和 同余 同余可以转化成等式存在 ,使得
一元线性同余方程
求解 ,这个同余方程等价于二元丢番图方程,方程有解
定理 设 和 是整数,。若 不能整除 ,则 无解;若 能够整除 ,则 有 个模 不同余的解
推论 互素,线性同余方程 有唯一的模 不同余的解。
逆
给定整数 ,且满足 ,则 的一个解为 模 的逆,记为
求逆
- [[#费马小定理]], 是素数,且 互素,求
- 扩展欧几里得, 互素
- 递推求逆 时,假设 ,余数是 ,则 因此 等价于 移项 等价于
逆可以用来求解同余方程,若,上边的同余方程可以适用下面的式子来求解 ,在模 下有唯一解
定理 设 是素数,正整数 是其自身的逆,当且仅当 或者 正如[[#二次探测定理]]
除法取模
中国剩余定理
假设有 两两互质 ,且
那么该方程有一个通用解 设 , 因此 一定与 互质,故M, M设 为 模 的逆元 因此可以构造答案
可以验证此 成立
拓展
中国剩余定理的拓展,因为剩余定理求解公式需要满足两两互质的强限制,因此需要更一般的解法
解法: 由于 等价于 , 其中 是系数 我们可取前两个等式考虑即 , ,合并得
整理得
根据裴蜀定理, 要想有整数解,则 为 的倍数 故可根据扩展欧几里得求出特解 因此可得通解 ,其中 k 是系数 带入到求解 的式子中得
整理得
可以发现,这是一个与 类似的式子,因此,便可以通过这种方式求解
细节注意: 的计算可以写成 ;计算 分开求,可能爆 long long;最后计算 最后对 取模;核心思想在于在这个过程中,让值变得更小
LL calc() {
LL a1 = a[1], m1 = m[1]; for (int i = 2; i <= n; i ++ ) { LL a2 = a[i], m2 = m[i]; LL a = a1, b = a2, c = (m2 - m1 % a2 + a2) % a2; LL x, y; LL d = exgcd(a, b, x, y); if (c % d) return -1; // b0 的计算也可以龟速乘 LL k = b / d, b0 = (c / d * x % k + k) % k; LL ans = a1 * k; // 下面这行可能爆longlong 不行就龟速乘 m1 = (a1 * b0 % ans + m1) % ans; a1 = ans; } return m1;}BSGS 算法
问题描述:若 ,求 求最小非负正整数解
根据欧拉定理, , 因此,最小的 在计算的时候可以稍微扩大成 ,避免计算欧拉函数
枚举是利用分块的思想,首先将这个区间分成 段,令 , ,为了让 能够取到 中的所有数,可以设置 ,这样 ,因为 。注意,0特判即可
原式可变形为 ,也就是说,枚举 的时候,判断是否有对应的 使其同余 相等
欧拉函数
中与 互质的数的个数被称为欧拉函数,记为 欧拉函数的计算公式为:
这里提到另外一个本人想到的公式,设 为小于 的约数,记 为 中 的 的个数。其计算公式为:
欧拉函数计算公式证明: 证明思路是计算 中有多少数是 的合数,最后用 减去这个结果即可。那么 中有多少是 的合数可以根据唯一分解定理计算,一个数是 的合数,一定是 的倍数,因此可以通过计算 中有多少数是这些数的倍数,减掉即可。即: 。但是,这样还不行,因为像 的倍数其实被删掉了两次,要重新加回来,这也是通过容斥原理来计算。因此,最终得到的公式为上述公式。
线性筛求欧拉函数
欧拉函数可以用筛素数的写法来计算欧拉函数,线性筛的本质是用一个数的最小质数将其筛掉,那么便可以思考一个数的欧拉函数值是否可以通过其最小质数和其相应约数计算得到。
首先,对于 为质数,那么 为 。
然后思考线性筛的过程,如果 时,会发生什么,对于 来说, 就是它的最小质数,但是事实上此时 是 的约数了,根据欧拉函数与指数没有关系,假设,,那么 。
如果 时,则说明 的质因数分解中原来没有 ,此时 ,那么 。
核心代码如下:
oid get_euler(int n) {
for (int i = 2; i <= n; i ++ ) { if (!st[i]) { primes[cnt ++ ] = i; phi[i] = i - 1; } for (int j = 0; i <= n / primes[j]; j ++ ) { st[i * primes[j]] = true; if (i % primes[j] == 0) { phi[i * primes[j]] = phi[i] * primes[j]; break; } phi[i * primes[j]] = phi[i] * (primes[j] - 1); } }}欧拉定理
设 , 且 , 为欧拉函数,有
欧拉降幂
莫比乌斯反演
莫比乌斯函数
根据[[#唯一分解定理]]
若 ,则
线性代数
矩阵快速幂
这里提供矩阵快速幂的模板。
struct matrix { int m[N][N]; };
matrix operator * (const matrix& a, const matrix& b) { matrix c; memset(c.m, 0, sizeof c.m); for (int i = 0; i < N; i ++ ) for (int j = 0; j < N; j ++ ) for (int k = 0; k < N; k ++ ) c.m[i][j] = (c.m[i][j] + 1ll * a.m[i][k] * b.m[k][j] % mod) % mod; return c;}
matrix pow_matrix(matrix a, int k) { matrix ans; memset(ans.m, 0, sizeof ans); for (int i = 0; i < N; i ++ ) ans.m[i][i] = 1; while (k) { if (k & 1) ans = ans * a; k >>= 1; a = a * a; } return ans;}这个模板是相当不错的,矩阵乘里面的 , mod 可以不是常量,可以在事后输入。
矩阵快速幂的应用
对于 这样的递推式子,容易构造矩阵
其中,矩阵 就是根据递推式构造的矩阵 有的时候,递推式就是根据动态规划转移式子得来,有的时候隐藏的很深,需要去发现去设计
高斯消元
在高斯消元的过程中,我们需要对判断最终的结果是唯一解?多解还是无解?这里有一个通用的判断方式(关系未知, 行 列)
// 首先判断在进行了多次处理后 最后 row ~ m 行的最后一列是否全为 0,不是则为无解 for (int i = row; i <= m; i ++ ) if (a[i][n + 1]) return 0; // row - 1 表示秩的大小,显然秩为 n 的时候为唯一解 if (row == n + 1) return 1; return 2;时间复杂度 ,其中 分别为行列
高斯消元求解线性方程组
代码如下:
/*输出解 注意 0 的判断for (int i = 1; i <= n; i ++ ) printf("%.3f ", fabs(a[i][n + 1]) < eps ? 0.000 : a[i][n + 1]);*/// 0 无解 1 唯一解 2 无穷解int guass() {
int row, col; for (col = 1, row = 1; col <= n; col ++ ) { int t = row; for (int i = row + 1; i <= n; i ++ ) if (fabs(a[i][col]) > fabs(a[t][col])) t = i;
if (fabs(a[t][col]) < eps) continue; for (int i = col; i <= n + 1; i ++ ) swap(a[t][i], a[row][i]); for (int i = n + 1; i >= col; i -- ) a[row][i] /= a[row][col];
for (int i = 1; i <= n; i ++ ) if (i != row && fabs(a[i][col]) > eps) { for (int j = n + 1; j >= col; j -- ) a[i][j] -= a[row][j] * a[i][col]; }
row ++ ; } if (row <= n) { for (int i = row; i <= n; i ++ ) if (fabs(a[i][n + 1]) > eps) return 0; return 2; } return 1;}高斯消元求解异或线性方程组
代码如下:
int guass() {
int col, row; for (col = row = 1; col <= n; col ++ ) { int t = row; for (int i = row + 1; i <= n; i ++ ) if (a[i][col] > a[t][col]) t = i;
if (a[t][col] == 0) continue; for (int i = col; i <= n + 1; i ++ ) swap(a[t][i], a[row][i]);
for (int i = 1; i <= n; i ++ ) if (i != row && a[i][col] == 1) { for (int j = n + 1; j >= col; j -- ) a[i][j] ^= a[row][j]; } row ++ ; }
if (row <= n) { for (int i = row; i <= n; i ++ ) if (a[i][n + 1] > 0) return 0; return 2; } return 1;}高斯消元求解模运算线性方程组
代码如下
// 注意这里是 模 3 的写法,如果是其他的模,需要求逆// 以下如果是模 p 的话,则 3 改为 pvoid gauss() {
int col, row; for (col = row = 0; col < n; col ++ ) { int t = row; for (int i = row + 1; i < n; i ++ ) if (a[i][col] > a[t][col]) t = i; if (a[t][col] == 0) continue;//print(); for (int i = col; i <= n; i ++ ) swap(a[t][i], a[row][i]); // 此处需要求逆,其中 2 替换为 a[row][col]对于 p 的逆 // 这个 if 需要去掉 if (a[row][col] == 2) { for (int i = n; i >= col; i -- ) a[row][i] = (a[row][i] * 2) % 3; } for (int i = 0; i < n; i ++ ) if (i != row && a[i][col]) { for (int j = n; j >= col; j -- ) a[i][j] = (a[i][j] + (3 - a[i][col]) * a[row][j]) % 3; } row ++ ;
}}线性基
迭代构造线性基
时间复杂度 ,其中 为 个数里面的最大位数,在 long long 范围内等价于
代码如下:
LL base[63]; // 存储线性基bool zero = false; // 查看是否这组数据里面能否出现 0
void insert(LL x) {
for (int i = 62; i >= 0; i -- ) { if (x >> i == 1) { if (base[i] == 0) {base[i] = x; return ;} else x = x ^ base[i]; } } zero = true;}
LL qmax() { LL res = 0; for (int i = 62; i >= 0; i -- ) res = max(res, res ^ base[i]); return res;}