算法备忘录·数学知识
本文最后更新于 2024年5月15日 凌晨
一直以来,没能系统学习算法,是我心中的痛;现在学了 y 总的算法基础课,做做笔记,希望能够有所收获。
这是第五节,数学知识。
尽管很多算法之前在离散数学中经常使用,但是从 OI 的视角来看,又是一种全新的感觉……
数学知识
质数
试除法判定质数
- 时间复杂度
- 模板
bool isPrime(int x){
if (x < 2) return false;
for (int i = 2; i <= x / i; i ++ ) // 使用 x / i 效果最好,注意是 <=
if (x % i == 0)
return false;
return true;
}
试除法分解质因数
- 时间复杂度
- 模板
void divide(int x){
for (int i = 2; i <= x / i; i ++ ){
int s = 0;
while (x % i == 0) x /= i, s ++;
cout << i << ' ' << s << endl;
}
if (x > 1) cout << x << ' ' << 1 << endl;
}
求素数
朴素筛法
- 时间复杂度
- 模板
int primes[N], cnt; // primes[] 存储所有素数
bool st[N]; // st[x] 存储 x 是否被筛掉
void get_primes(int n){
for (int i = 2; i <= n; i ++ ){
if (!st[i]) primes[cnt ++ ] = i;
for (int j = i + i; j <= n; j += i){
st[j] = true;
}
}
}
埃氏筛
- 时间复杂度
- 模板
int primes[N], cnt; // primes[] 存储所有素数
bool st[N]; // st[x] 存储 x 是否被筛掉
void get_primes(int n){
for (int i = 2; i <= n; i ++ ){
if (!st[i]){
primes[cnt ++ ] = i;
for (int j = i + i; j <= n; j += i){
st[j] = true;
}
}
}
}
线性筛
- 时间复杂度
- 模板
int primes[N], cnt; // primes[] 存储所有素数
bool st[N]; // st[x] 存储 x 是否被筛掉
voidget_primes(int n){
for (int i = 2; i <= n; i ++ ){
if (!st[i]) primes[cnt ++ ] = i;
for (int j = 0; primes[j] <= n / i; j ++ ){
st[primes[j] * i] == true;
if (i % primes[j] == 0) break;
}
}
}
约数
试除法求所有约数
- 模板
vector<int> get_divisors(int x){
vector<int> res;
for (int i = 1; i <= x / i; i ++ ){
if (x % i == 0){
res.push_back(i);
if (i ! = x / i) res.push_back(x / i);
}
}
sort(res.begin(), res.end());
return res;
}
int 范围内约数最多的个数约 1500 个
约数个数、约数之和
- 如果
- 约数个数:
- 约数之和:
欧几里得算法求最大公约数
- 模板
int gcd(int a, int b){
return b ? gcd(b, a % b) : a
}
欧拉函数
- ,则 。 是 1 ~ 与 互质的数的个数,被称为欧拉函数。
- 容斥原理证明
计算欧拉函数
定义法
- 时间复杂度
- 模板
int phi(int x){
int res = x;
for (int i = 2; i <= x / i; i ++ ){
if (x % i == 0){
res = res / i * (i - 1);
while (x % i == 0) x /= i;
}
}
if (x > 1) res = res / x * (x - 1);
return res;
}
筛法批量求欧拉函数
- 时间复杂度
- 基于线性筛
- 模板
int primes[N], cnt; // primes[] 存储所有素数
int euler[N]; // euler[] 存储每个数的欧拉函数
bool st[N];
void get_eulers(int n){
euler[1] = 1; // 1 的欧拉函数值是 1
for (int i = 2; i <= n; i ++ ){
if (!st[i]){
primes[cnt ++ ] = i;
euler[i] = i - 1;
}
for (int j = 0; primes[j] <= n / i; j ++ ){
int t = primes[j] * i;
st[t] = true;
if (i % primes[j] == 0){
euler[t] = euler[i] * primes[j];
break;
}
euler[t] = euler[i] * (primes[j] - 1);
}
}
}
快速幂
快速幂算法
- 快速求 ,时间复杂度
- 模板
int qmi(int m, int k, int p){
int res = 1 % p, t = m;
while (k){
if (k & 1) res = res * t % p;
t = t * t % p;
k >> = 1;
}
return res;
}
扩展欧几里得算法
- 裴蜀定理:对于任意正整数 ,一定存在整数 ,使得
扩展欧几里得算法
- 模板
int exgcd(int a, int b, int &x, int &y){
if (!b){
x = 1, y = 0;
return a;
}
int d = exgcd(b, a % b, y, x);
y -= (a / b) * x;
return d;
}
高斯消元
高斯消元解线性方程组
- 时间复杂度
- 步骤
- 枚举每一列
- 找到绝对值最大的一行
- 将该行换到最上面
- 将该行第一个数变成 1
- 把下面所有行的第 列变成 0
- 模板
// a[N][N]是增广矩阵
int gauss(){
int c, r;
for (c = 0, r = 0; c < n; c ++ ){
int t = r;
for (int i = r; i < n; i ++ ){
if (fabs(a[i][c]) > fabs(a[t][c])) t = i; // 找到绝对值最大的一行
}
if (fabs(a[t][c]) < eps) continue; //如果第 c 列是 0, 就直接考虑下一列
for (int i = c; i <= n; i ++ ) swap(a[t][i], a[r][i]); // 将该行换到最上面
for (int i = n; i >= c; i -- ) a[r][i] /= a[r][c]; // 将该行第一个数变成 1
for (int i = r; i <= n; i ++ )
if (fabs(a[i][c]) > eps)
for (int j = c; j >= c; j --)
a[i][j] -= a[r][j] * a[i][c];
r ++;
}
if (r < n){
for (int i = r; i < n; i ++ ){
if (fabs(a[i][n]) > eps)
return 2; // 无解
}
return 1; // 有无穷多组解
}
for (int i = n - 1; i >= 0; i -- )
for (int j = i + 1; j < n; j ++ )
a[i][n] -= a[i][j] * a[j][n];
return 0; // 有唯一解
}
组合数
递归法求组合数
- 原理:
- 时间复杂度
- 模板
// c[a][b] 表示从a个苹果中选b个的方案数
for (int i = 0; i < N; i ++ )
for (int j = 0; j <= i; j ++ )
if (!j) c[i][j] = 1;
else c[i][j] = (c[i - 1][j] + c[i - 1][j - 1]) % mod;
通过预处理逆元的方式求组合数
- 原理:。所以可以预处理阶乘,然后计算
- 时间复杂度
- 模板
// 快速幂模板
int qmi(int a, int k, int p){
int res = 1;
while (k){
if (k & 1) res = (LL)res * a %p;
a = (LL)a * a % p;
k >>= 1;
}
return res;
}
// 预处理阶乘的余数和阶乘逆元的余数
fact[0] = infact[0] = 1;
for (int i = 1; i < N; i ++ ){
fact[i] = fact[i - 1] * i;
infact[i] = infact[i - 1] * qmi(i, mod - 2, mod) % mod;
}
Lucas 定理求组合数
- 原理:
- 时间复杂度:
- 模板
// 快速幂模板
int qmi(int a, int k, int p){
int res = 1;
while (k){
if (k & 1) res = (LL)res * a %p;
a = (LL)a * a % p;
k >>= 1;
}
return res;
}
// 通过定义求组合数C(a, b)
int C(int a, int b, int p){
if (a < b) return 0;
LL x = 1, y = 1; // x是分子,y是分母
for (int i = a, j = 1; i <= b; i--, j ++ ){
x = (LL)x * i % p;
y = (LL)y * j % p;
}
return x * (LL)qmi(y, p - 2, p) % p;
}
int lucas(LL a, LL b, int p){
if (a < p && b < p) return C(a, b);
return (LL)C(a % p, b % p) * lucas(a / p, b / p, p) % p;
}
分解质因数法求组合数
-
需要求出组合数的真实值,而非对某个数的余数时,分解质因数的方式比较好用
-
步骤
- 筛法求出范围内的所有质数
- 通过 这个公式求出每个质因子的次数。
- 中 的次数是
- 用高精度乘法将所有质因子相乘
-
模板
int primes[N], cnt; // 存储所有质数
int sum[N]; //存储每个质数的次数
bool st[N]; // 存储每个数是否已被筛掉
// 线性筛
void get_primes(int n){
for (int i = 2; i <= n; i ++){
if (!st[i]) primes[cnt ++ ] = i;
for (int j = 0; primes[j] <= n / i; j ++ ){
st[primes[j] * i] = true;
if (i % primes[j] == 0) break;
}
}
}
// 求 n! 中素数 p 的次数
int get(int n, int p){
int res = 0;
while (n){
res += n / p;
n /= p;
}
return res;
}
// 高精度乘法
vector<int> mul(vector<int> a, b){
vector<int> c;
int t = 0;
for (int i = 0; i < a.size(); i ++ ){
t += a[i] * b;
c.push_back(t % 10);
t /= 10;
}
while (t){
c.push_back(t % 10);
t /= 10;
}
return c;
}
get_primes(a);
for (int i = 0; i < cnt; i ++ ){
int p = primes[i];
sum[i] = get(a, p) - get(b, p) - get(a - b, p);
}
vector<int> res;
res.push_back(1);
for (int i = 0; i < cnt; i ++ )
for (int j = 0; j < sum[i]; j ++ )
res = mul(res, primes[i]);
卡特兰数
- 通项公式:
- 所有的卡特兰数问题经过一定的转换都可以还原成进出栈问题
- 举例:给定 个 0 和 个 1,它们按照某种顺序排成长度为 的序列,满足任意前缀中 的个数都不少于 1 的个数的序列的数量
容斥原理
-
容斥原理:
- 用组合数恒等式证明
-
在应用时,对集合的枚举可以使用位运算来表示
博弈论
Nim 游戏
-
给定 堆物品,第 堆物品有 个。两名玩家轮流行动,每次可以任选一堆,取走任意多个物品,可把一堆取光,但不能不取。取走最后一件物品者获胜。
-
定理: Nim 博弈先手必胜,当且仅当
有向图游戏
-
给定一个有向无环图,图中有一个唯一的起点,在起点上放有一枚棋子。两名玩家交替地把这枚棋子沿有向边进行移动,每次可以移动一步,无法移动者判负。该游戏被称为有向图游戏。
-
任何一个公平组合游戏都可以转化为有向图游戏。
mex 运算
- mex 运算: 表示不属于集合 的最小自然数,即 。
SG 函数
- SG 函数:在有向图游戏中, 对于每个节点 , 设从 出发共有 条有向边, 分别到达节点 , 定义 为 的后继节点 的 函数值构成的集合再执行 运算的结果,即:
有向图游戏的和
- 设 是 个有向图游戏。定义有向图游戏 ,它的行动规则是任选某个有向图游戏 ,并在 上行动一步。 被称为有向图游戏 的和。
- 有向图游戏的和的 SG 函数值等于它包含的各个子游戏 SG 函数值的异或和, 即:
有向图游戏必胜
-
有向图游戏的某个局面必胜,当且仅当该局面对应节点的SG函数值大于 0;
-
有向图游戏的某个局面必败*,当且仅当该局面对应节点的SG函数值等于** 0。
-
解释:
- 非 0 可以走向 0
- 0 只能走向非 0
-
模板
int s[N], f[N]; // s[]表示每次能够拿取石子数的集合;f[] 记录石子数为 x 堆的 sg 函数值
memset(f, -1, sizeof f);
// 记忆化搜索
int sg(int x){
if (f[x] != -1) return f[x]; // 已被计算过
unordered_set<int> S;
for (int i = 0; i < m; i ++ ){
int sum = s[i];
if (x >= sum) S.insert(sg(x - sum));
}
// mex(x)
for (int i = 0; ; i ++ )
if (!S.count(i))
return f[x] = i;
}
// 把 n 堆石子看成 n 个独立的有向图 G,把各个有向图结果做异或得到答案
int x, res = 0;
for (int i = 0; i < n; i ++ )
{
cin >> x;
res ^= sg(x);
}
算法备忘录系列目录:
第一节 算法备忘录·基础算法
第二节 算法备忘录·数据结构
第三节 算法备忘录·杂项
第四节 算法备忘录·搜索与图论
第五节 算法备忘录·数学知识
第六节 算法备忘录·动态规划
第七节 算法备忘录·贪心
第八节 算法备忘录·时空复杂度分析
第九节 算法备忘录·杂项
算法备忘录·数学知识
https://justloseit.top/算法备忘录·数学知识/