部分内容来自牛客相关课程课件
这个笔记想写得稍微详细一点,部分内容来自我自己之前的博客: OI 里的数学内容整理(提高组)。
其中一些公式是 AI 从 PPT 上识别的,格式混乱,还请谅解。
整数分解与筛法
欧几里得算法
用于求解两个数的最大公因数。
int gcd(int a,int b){ return b==0?a:gcd(b,a%b); }
扩展欧几里得算法与裴蜀定理
用于求解
int exgcd(int a,int b,int &x,int &y){//注意:结果x,y可能是负数,但是输入的a,b需要均为正数!!! if(b==0){x=1;y=0;return a;} int GCD=exgcd(b,a%b,x,y); int tmp;tmp=x; x=y;y=tmp-a/b*y; return GCD; }
也可以用于求逆元:
求
例题:青蛙的约会
裴蜀定理(前置定理):对于
裴蜀定理扩展:对于
算术基本定理(整数的分解)
任何一个大于
埃氏筛
bool not_prime[N]; int tot; int pri[N]; void Sieve_of_Eratosthenes(int nn){ for(int i=2;i<=nn;i++){ if(!not_prime[i]){ pri[++tot]=i; for(int j=2*i;j<=n;j+=i){ not_prime[j]=1; } } } }
时间复杂度:
欧拉筛
bool not_prime[N]; int tot; int pri[N],phi[N]; void Euler_Sieve(int nn){ for(int i=2;i<=nn;i++){ if(!not_prime[i]) pri[++tot]=i,phi[i]=i-1; for(int j=1;j<=tot&&i*pri[j]<=nn;j++){ not_prime[pri[j]*i]=1; if(i%pri[j]==0){ phi[i*pri[j]]=phi[i]*pri[j]; break; } phi[i*pri[j]]=phi[i]*(pri[j]-1); } } }
时间复杂度:
例题:Sum of Consecutive Prime Numbers
质因数分解
暴力
例题:Prime Land
Stein 算法(大整数的 GCD)
- 如果
或 ,则 - 如果
均为偶数,则 - 如果
是偶数, 是奇数,则 - 如果
是奇数, 是偶数,则 - 如果
均为奇数,不妨设 则
时间复杂度为
【补充】数论分块
对于一些整除类问题,如果出现了
void division_block(ll n){ for(ll l=1,r;l<=n;l=r+1){ r=n/(n/l); //do something //在 l~r 这个范围中,所有的 n/i 得到的数值都是一样的 } }
同余与模
逆元
若
可以通过扩展欧几里得算法求出。若
但如果要求出
ll inv[N]; void init_inv(int nn,int p){ inv[1]=1; for(int i=2;i<=nn;i++){ inv[i]=(ll)(p-p/i)*inv[p%i]%mod; } }
该算法利用了
推导过程:
模板题:乘法逆元
欧拉函数
记
通式:
记
容易发现,当
可以通过欧拉筛线性求出所有欧拉函数(见上文),但如果只需要求一项欧拉函数,也可以暴力求。
ll phi(int x){ int ret=1,ori=x; for(int i=2;i*i<=ori;i++){ int cnt=0; while(x%i==0){ x/=i;cnt++; if(cnt==1) ret*=(i-1); else ret*=i; } } if(x>1) ret*=(x-1); return ret; }
欧拉定理和费马小定理
欧拉定理:对于任意互素的
欧拉定理推论:若
费马小定理:若
费马小定理求逆元:若
同余方程组与扩展中国剩余定理
求解
利用
int msc(int a,int b,int mod){//慢速乘,用于防止乘法溢出 if(a<0&&b<0) a=-a,b=-b; else if(b<0) swap(a,b); int ret=0; while(b){ if(b&1) ret=(ret+a)%mod; a=(a+a)%mod;b>>=1; } return ret; } int gcd(int a,int b){ return b==0?a:gcd(b,a%b); } int lcm(int a,int b){ return a/gcd(a,b)*b; } int exgcd(int a,int b,int &x,int &y){//注意:结果x,y可能是负数,但是输入的a,b需要均为正数!!! if(b==0){x=1;y=0;return a;} int GCD=exgcd(b,a%b,x,y); int tmp;tmp=x; x=y;y=tmp-a/b*y; return GCD; } void merge_equation(int &A,int &B,int a,int b){ int y1,y2; int d=exgcd(B,b,y1,y2); if((a-A)%d!=0){A=-1;return;} y1=msc(y1,(a-A)/d,b/d); int m=lcm(B,b); A=((B*y1+A)%m+m)%m; B=m; } pi excrt(int nn,int a[],int b[]){//x=a(mod b) int A,B; for(int i=1;i<=nn;i++){ if(i==1) A=a[i],B=b[i]; else merge_equation(A,B,a[i],b[i]); if(A==-1) return mk(-1,-1); } return mk(A,B); }
模板题:扩展中国剩余定理
例题:Biorhythms
中国剩余定理
求解
令
显然
于是有:
进一步:
因此解为
扩展欧拉定理
例题:Power Tower
在题目中,你可以通过修改快速幂的代码来帮助使用扩展欧拉定理,具体如下:
ll ksm(ll a,ll b,int p){ ll ret=1; while(b){ if(b&1){ret=ret*a;if(ret>=p) ret=ret%p+p;} a=a*a;if(a>=p) a=a%p+p; b>>=1; } return ret; }
该代码计算的是
威尔逊定理
排列组合与容斥原理
约定符号
- 下降幂:
- 上升幂:
- 阶乘:
注:
加法和乘法原理
加法原理:做某件事情有几种选择,每种选择的方案数之和就是做这件事情的方案数。
乘法原理:做某件事情分为几步,每步的方案数是独立的,则它们的积就是做这件事情的方案数。
排列与组合
排列数:
组合数:
容斥原理
基础形式
设
符号形式
设
- 记
为 中有 性质的元素的数量。特殊的,记 。 - 记
为 中没有 性质的元素的数量。 为 S 中同时有 性质的元素的数量。- 记
。
则容斥原理可以写成
如果需要有其中一些性质,而不需要其他的性质,可以写作
例题:大水题
第二类斯特林数
有
- 记号
, - 递推式
- 容斥
- 重要的公式
积性函数
定义与求法
积性函数如果函数
使用质因数分解求解单个
int calc_f(int x,int y){//计算 f(x^y) //return y+1; } ll get_f(ll nn) { ll ans=1; for (int i=2; i*i<=nn;i++){ int cnt=0; while (nn%i==0) cnt++,nn/=i; if(cnt!=0) ans*=calc_f(i,cnt); } if (nn>1) ans*=f(nn,1); return ans; }
使用欧拉筛求解多个
int f[N]; bool not_prime[N]; int tot; int pri[N],cnt[N];//cnt 表示最小的质因子的次数 int calc_f(int x,int y){//计算 f(x^y) //return y+1; } void Euler_Sieve(int nn){ f[1]=1;//根据情况定初始值 for(int i=2;i<=nn;i++){ if(!not_prime[i]) pri[++tot]=i,f[i]=calc_f(i,1),cnt[i]=1; for(int j=1;j<=tot&&i*pri[j]<=nn;j++){ not_prime[pri[j]*i]=1; if(i%pri[j]==0){ cnt[i*pri[j]]=cnt[i]+1; f[i*pri[j]]=f[i]/calc_f(pri[j],cnt[i])*calc_f(pri[j],cnt[i]+1); break; } cnt[i*pri[j]]=1; f[i*pri[j]]=f[i]*calc_f(pri[j],1); } } }
模板题:【线性筛求积性函数】
例题:华华给月月出题
莫比乌斯反演
前置定义(莫比乌斯函数):
举例:
莫比乌斯反演 形式一
设
莫比乌斯反演 形式二
设
例题:序列
常见的积性函数
- 单位函数:
常数函数:
恒等函数:
幂函数:
约数函数:
约数个数函数:
欧拉函数:
或莫比乌斯函数:
其中
狄利克雷卷积
设
定理
- 如果
是积性函数,则 也是积性函数。 - $f=g1\Leftrightarrow g=f\mu$
常用公式
杜教筛
对于 $M (n) = \sum {i= 1}^n\mu ( i)
例题:LCMs
矩阵与高斯消元
这部分的内容基本是线性代数的内容,故简略叙述。
矩阵的定义、乘法与快速幂
矩阵的定义:
struct Mat{ ll a[MATN][MATN]; Mat(){ for(int i=0;i<MATN;i++){ for(int j=0;j<MATN;j++){ a[i][j]=0; } } for(int i=0;i<MATN;i++) a[i][i]=1; } Mat(ll a1,ll a2,ll a3,ll a4){ a[0][0]=a1;a[0][1]=a2; a[1][0]=a3;a[1][1]=a4; } }a[N];
矩阵的乘法:
Mat operator *(Mat x,Mat y){ Mat z; for(int i=0;i<MATN;i++){ for(int j=0;j<MATN;j++){ z.a[i][j]=inf; } } for(int i=0;i<MATN;i++){ for(int j=0;j<MATN;j++){ for(int k=0;k<MATN;k++){ z.a[i][j]=min(z.a[i][j],x.a[i][k]+y.a[k][j]); } } } return z; };
矩阵快速幂:将普通快速幂中的运算对象改为矩阵即可,不过要注意左乘还是右乘,这在矩阵中是不同的。
矩阵快速幂能够有效优化先行递推,可以用于动态 DP 或者是一些数据结构与矩阵的结合问题。
例题:Magic Gems
线性方程组
高斯消元
高斯消元是求解线性方程组的基本方法,也可以用来求解异或方程组(甚至更简单)。
for(int i=1;i<=n;i++){ int maxx=i; for(int j=i+1;j<=n;j++){ if(fabs(a[j][i])>fabs(a[maxx][i])) maxx=j; } if(i!=maxx) swap(a[i],a[maxx]); if(fabs(a[i][i])<eps){ printf("No Solution\n"); return 0; } for(int j=i+1;j<=n+1;j++) a[i][j]/=a[i][i]; for(int j=1;j<=n;j++){ if(j==i) continue; for(int k=i+1;k<=n+1;k++){ a[j][k]-=a[j][i]*a[i][k]; } } }
行列式与逆矩阵
行列式:我们可以用高斯消元的方法将矩阵化为上三角矩阵,从而快速求出行列式。
逆矩阵:构造出增广矩阵
线性基
向量空间的基:向量空间中最大的线性无关组称为向量空间的一组基。
线性基:一般指
通过线性基,可以 求一个集合
例题:xor 序列
多项式与生成函数
生成函数
- 多项式
- 形式幂级数
常生成函数:一个数列
对应的常生成函数为- 形式幂级数
的逆元: - 逆元存在的条件:
- 暴力计算的方法:递推
- Catalan 数:
- 生成函数:
- 通项公式:
- 生成函数:
- 一个数列
对应的指数生成函数为
FFT(快速傅里叶变换)
FFT 通常包含 DFT 和 IDFT。
DFT(离散傅里叶变换):将多项式
IDFT 是 DFT 的逆过程,同样可以使用 FFT 解决。
对于两个多项式相乘的问题,通过 分别 DFT -> 将结果依次相乘 -> IDFT 的步骤可以在
不过由于 FFT 使用的是虚数运算,故精度较低。
NTT(快速数论变换)
NTT 和 FFT 一样,用于解决两个多项式相乘的问题,不过 NTT 是在模数意义下进行的,其要求模数是满足
拉格朗日插值定理
- 暴力实现:先计算
, 再求 次 , 按照权值和将它们相加。 特殊情况 1: 只需要求一个
。- 特殊情况 2: 只需要求一个
, 且 。
阶与原根
- 阶:若正整数
, 满足 , 则使得 (mod 的最小的正整数 称为 模 的阶,记作 。 - 原根:若
, 则称 为 的一个原根。 假设
, , 则 在模 意义下两两不同-
- 原根的存在定理:只有模
存在原根 - 原根的判定定理:设
, 为正整数且 。则 是 的原根当且仅当对于任意 的质因子 - 指标:对于质数
, 假设 是 的一个原根,则 在模 意义下是 的
一个排列。假设对于 有 , 则称 的指标为 , 记作
代码
FFT 和 NTT 代码来自于 A999999。
FFT
#include <algorithm> #include <iostream> #include <vector> #include <cmath> const double PI = std::acos(-1); class Complex{ public: Complex(double _real = 0, double _virtual = 0); double getReal()const; double getVirtual()const; Complex operator +(const Complex& b)const; Complex operator -(const Complex& b)const; Complex operator *(const Complex& b)const; private: double x, y; }; Complex::Complex(double _real, double _virtual): x(_real), y(_virtual){} double Complex::getReal()const{ return this->x;} double Complex::getVirtual()const{ return this->y;} Complex Complex::operator +(const Complex& b)const{ return Complex(this->x + b.x, this->y + b.y); } Complex Complex::operator -(const Complex& b)const{ return Complex(this->x - b.x, this->y - b.y); } Complex Complex::operator *(const Complex& b)const{ return Complex(this->x * b.x - this->y * b.y, this->x * b.y + this->y * b.x); } /* recursive FFT void FFT(std::vector<Complex>& A, int flag = 1){ int n = A.size(); if(n == 1) return; std::vector<Complex> A1(n >> 1), A2(n >> 1); for(int i = 0; i < (n >> 1); ++i) A1[i] = A[i << 1], A2[i] = A[i << 1 | 1]; FFT(A1, flag); FFT(A2, flag); Complex w1(std::cos(2.0 * PI / n), std::sin(2.0 * PI / n) * flag), wk(1, 0); for(int k = 0; k < (n >> 1); wk = wk * w1, ++k) A[k] = A1[k] + A2[k] * wk, A[k + (n >> 1)] = A1[k] - A2[k] * wk; } */ // make reversed binary representation array std::vector<int> makerev(const int& len){ std::vector<int> ans; ans.emplace_back(0); ans.emplace_back(len >> 1); int l = 0; while((1 << l) < len) l++; for(int i = 2; i < len; ++i) ans.emplace_back(ans[i >> 1] >> 1 | (i & 1) << (l - 1)); /* ans[i >> 1] is the reversed representation of i >> 1 (i >> 1) << 1 = i, so in reversed representation we need ans[i >> 1] >> 1 if i & 1 == 1, then the MSB of reversed representation should be 1 that is (i & 1) << (l - 1) */ return ans; } // iterative FFT void FFT(std::vector<Complex>& A, const int& flag = 1){ static std::vector<int> rev; int n = A.size(); if(int(rev.size()) != n) rev.clear(), rev = makerev(n); for(int i = 0; i < n; ++i) if(rev[i] > i) std::swap(A[i], A[rev[i]]); for(int len = 2, m = 1; len <= n; m = len, len <<= 1){ Complex w1(std::cos(2.0 * PI / len), std::sin(2.0 * PI / len) * flag), wk; for(int l = 0, r = len; r <= n; l += len, r += len){ wk = Complex(1, 0); for(int k = l; k < l + m; wk = wk * w1, ++k){ Complex x = A[k] + A[k + m] * wk, y = A[k] - A[k + m] * wk; A[k] = x; A[k + m] = y; } } } } signed main(int argc, char ** argv){ std::cin.tie(nullptr)->sync_with_stdio(false); int n, m, len = 1; std::cin >> n >> m; while(len <= (n + m)) len <<= 1;// to make the length a power of 2 std::vector<Complex> A(len), B(len); for(int i = 0, x; i <= n; ++i){ std::cin >> x; A[i] = Complex(x, 0); } for(int i = 0, x; i <= m; ++i){ std::cin >> x; B[i] = Complex(x, 0); } FFT(A); FFT(B); for(int i = 0; i < len; ++i) A[i] = A[i] * B[i]; FFT(A, -1); for(int i = 0; i <= (n + m); ++i) std::cout << int(A[i].getReal() / len + 0.5) << ' '; std::cout << std::endl; return 0; }
NTT
#include <algorithm> #include <iostream> #include <vector> typedef long long i64; /// quick power /// @brief /// @tparam MOD /// @param x /// @param y /// @return template<i64 MOD> i64 qpow(i64 x, i64 y){ i64 ans = 1; while(y){ if(y & 1) ans = ans * x % MOD; x = x * x % MOD; y >>= 1; } return ans; } // make reversed binary representation array /// @brief /// @param rev /// @param len void makerev(std::vector<int>& rev, const int& len){ rev.resize(len); rev[0] = 0; rev[1] = len >> 1; int l = 0; while((1 << l) < len) l++; for(int i = 2; i < len; ++i) rev[i] = (rev[i >> 1] >> 1) | ((i & 1) << (l - 1)); /* rev[i >> 1] is the reversed representation of i >> 1 (i >> 1) << 1 = i, so in reversed representation we need rev[i >> 1] >> 1 if i & 1 == 1, then the MSB of reversed representation should be 1 that is (i & 1) << (l - 1) */ } // iterative NTT /// @brief /// @tparam MOD /// @param A /// @param flag template<i64 MOD> void NTT(std::vector<i64>& A, const int& flag = 1){ static std::vector<int> rev; static const i64 prt = 3, invprt = qpow<MOD>(prt, MOD - 2); int n = A.size(); if(int(rev.size()) != n) makerev(rev, n); for(int i = 0; i < n; ++i) if(rev[i] > i) std::swap(A[i], A[rev[i]]); for(int len = 2, m = 1; len <= n; m = len, len <<= 1){ i64 w1 = qpow<MOD>(flag == 1? prt: invprt, (MOD - 1)/ len), wk; for(int l = 0, r = len, k; r <= n; l += len, r += len){ for(k = l, wk = 1; k < l + m; wk = wk * w1 % MOD, ++k){ i64 x = A[k], y = A[k + m] * wk % MOD; A[k] = (x + y) % MOD; A[k + m] = (x - y + MOD) % MOD; } } } } const i64 mod = 998244353; signed main(int argc, char ** argv){ std::cin.tie(nullptr)->sync_with_stdio(false); int n, m, len = 1; std::cin >> n >> m; while(len <= (n + m)) len <<= 1;// to make the length a power of 2 std::vector<i64> A(len), B(len); for(int i = 0; i <= n; ++i) std::cin >> A[i]; for(int i = 0; i <= m; ++i) std::cin >> B[i]; NTT<mod>(A); NTT<mod>(B); for(int i = 0; i < len; ++i) A[i] = A[i] * B[i] % mod; NTT<mod>(A, -1); i64 invlen = qpow<mod>(len, mod - 2); for(int i = 0; i <= (n + m); ++i) std::cout << A[i] * invlen % mod << ' '; std::cout << std::endl; return 0; }