1.最大公因数( gcd):
解法:辗转相除法:
原理: gcd(a,b)=gcd(b,a%b)
证明:
假设 a>b ,那么 a 可以表示成 a=k∗b+r( a,b,k,r 皆为正整数,且 r<b),
则 r=a%b。
假设 d 是 a,b 的一个公约数,记作 d∣a,d∣b。
而 r=a−k∗b,两边同时除以 d,得: dr=da−dk∗b=m;
由等式右边可知 m 为整数,因此 d∣r;
因此 d 也是 b,a%b 的公约数;
假设 d 是 b,a%b 的公约数, 则 d∣b,d∣(a−k∗b) 是一个整数,进而 d∣a 。
因此 d 也是 a,b 的公约数;
因此 (a,b) 和 (b,a%b) 的公约数是一样的,其最大公约数也必然相等,
得证。
代码:
int _gcd(int x,int y)
{
return y?_gcd(y,x%y):x;
}
也可以直接利用 algorithm 库中的函数 __gcd(x,y)。
2.最小公倍数( lcm):
两个数 a,b 的最小公倍数:
ans=a/gcd(a,b)*b;//注意先除后乘,否则容易溢出
3.唯一分解定理:
定义:
任意一个大于 0 的正整数都能被表示成若干个素数的乘积且表示方法是唯一的。
即 n=p1a1∗p2a2∗...∗pkak
应用:
(1) gcd 为 a,b 两数所有相同质因子最小指数幂的乘积。
(2) lcm 为 a,b 两数所有的质因子的最大指数幂的乘积。
可以拓展到多个数。
(3)一个数因子个数:
n=(1+a1)∗(1+a2)∗(1+a3)∗...∗(1+ak)
(4)一个数的所有因子和:
n=i=1∏kj=0∑aipij=(1+p11+p12+...+p1a1)∗(1+p21+p22+...+p2a2)∗...(1+pk1+pk2+...+pkak)
质因子分解代码:
复杂度: O(n21)
//注意初始化
cnt=0;
for(int i=2;i*i<=num;i++)//sqrt(n)的复杂度
{
if(num%i==0)
{
factor[++cnt]=i;//存质因子
while(tn%i==0)
{
e[cnt]++;//质因子的幂
num/=i;
}
}
}
if(num>1)//不能省略!
{
factor[++cnt]=num;
e[cnt]++;
}
还可以预处理出素数表来加快分解速度。
大整数质因子分解:Pollard Rho因数分解
4.素数筛法:
埃氏筛:
实现:
首先将 2 到 n 范围内的整数写下来,其中 2 是最小的素数。将所有的 2 的倍数筛去,剩下的最小的数字就是 3 ,它不能被更小的数整除,所以 3 是素数。再将所有的 3 的倍数划去……以此类推。如果剩余的最小的数是 m,那么 m 就是素数。然后将所有 m 的倍数划去,像这样反复操作,就能依次枚举 n 以内的素数。
时间复杂度: O(nloglogn)
代码:
#include <bits/stdc++.h>
#define pb push_back
using namespace std;
const int N=1e5+5;
vector<int>prime;//存素数
bool vis[N];//vis=false为素数
void Sieve()
{
memset(vis,0,sizeof(vis));
vis[1]=1;
for(int i=2;i<N;i++)
{
if(!vis[i])
{
prime.pb(i);
for(int j=i;j<N;j+=i)//建议用加法
//for(int j=1;j*i<N;j++)
vis[j]=1;
}
}
}
int main()
{
Sieve();
for(int i=0;i<10;i++)
cout<<prime[i]<<endl;
return 0;
}
不足:一个数可能会被不同的质因子筛多次,造成时间的浪费。
欧拉筛(线性筛):
保证了每个合数只会被它的最小素因子筛掉,就把复杂度降到了 O(N)。
代码:
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
bool vis[N];//标记非素数
vector<int>prime;//保存素数
void euler()
{
prime.clear();
memset(vis,0,sizeof(vis));
for(int i=2;i<N;i++)
{
if(!vis[i])
prime.push_back(i);
for(int j=0;j<prime.size()&&i*prime[j]<N;j++)
{
vis[i*prime[j]]=1;
if(i%prime[j]==0)//重点
break;
}
}
}
int main()
{
euler();
return 0;
}
应用:
求积性函数。
5.快速幂:
定义:
要求 ab,朴素做法是把 a 连乘 b 次。时间复杂度为 O(n)。
把 b 拆成二进制,该二进制数第 i 位的权为 2i。
所以 ab=a2k+...+20
代码:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
ll power(ll a,ll b,ll mod)
{
ll res=1;
while(b)
{
if(b&1)
res=res*a%mod;
a=a*a%mod;
b>>=1;
}
return res%mod;
}
int main()
{
cout<<power(1LL*100,1LL*100,1LL*11)<<endl;
return 0;
}
6.逆元(乘法逆元):
定义:
若 a∗x=1(modp),则 x 称为 a 在模 p 意义下的逆元,即 x=inv(a)。
(a+b)%p=(a%p+b%p)%p (√)
(a−b)%p=(a%p−b%p)%p (√)
(a∗b)%p=(a%p∗b%p)%p (√)
(a/b)%p=(a%p/b%p)%p ( ×)
求法:
(1)费马小定理:
当 p 为素数, a 为正整数,且 a,p 互质时,有
a(p−1)=1(modp)
故 inv(a)=ap−2。
用快速幂取模求解即可。
复杂度: O(logn)
(2)拓展欧几里得:
a∗x=1(modp)⟹a∗x+p∗y=1
转化和求 x 的最小正整数解(转化为 [0,p) 内即可)。
要求满足 gcd(a,p)=1,否则方程无解,逆元不存在。
复杂度: O(logn)
(3)递推求逆元:
求 [1,p) 内的逆元。
复杂度: O(n)
公式: inv[i]=(p−p/i)∗inv[p%i]%p
推导:
设 p=k∗i+r,有
k∗i+r=0(modp)
即 r=−k∗i(modp)
两边同时除以 r∗i,有 inv[i]=−k∗inv[r](modp)
将 k,r 替换掉, inv[i]=(−k)∗inv[p%i]%p
因为 (−k)%p=(p−k)%p,所以有 inv[i]=(p−p/i)∗inv[p%i]%p
初始化 inv[1]=1,即可递推求出所有逆元。
补充: [1,p) 内的所有数的逆元对于 [1,p) 内的所有数。
代码:
void get_inv()
{
inv[1]=1;
for(int i=2;i<p;i++)
inv[i]=(p-p/i)*inv[p%i]%p;
}
7.素数分布定理:
定义:
对正整数 x,记 π(x) 为不大于 x 的素数个数。
x→∞limx/ln(x)π(x)=1
大致分布表:
x | π(x) |
---|---|
101 | 4 |
102 | 25 |
103 | 168 |
104 | 1,229 |
105 | 9,592 |
106 | 78,498 |
107 | 644,579 |
108 | 5,761,455 |
109 | 50,847,534 |
1010 | 455,052,511 |
例题:2019 ICPC Asia Xuzhou Regional C. <3 numbers
8.组合数:
组合数公式:
C(n,m)=m!n∗(n−1)∗(n−2)∗..∗(n−m+1)=(n−m)!∗m!n!(1)
C(n,m)=C(n,n−m)(2)
线性递推:
C(n,k)=C(n,k−1)∗k(n−k+1)
杨辉三角:
递推式: C(n,m)=C(n−1,m)+C(n−1,m−1)
复杂度: O(n2)
代码:
void init()
{
for(int i=0;i<=n;i++)
{
C[i][0]=C[i][i]=1;
for(int j=1;j<i;j++)
C[i][j]=C[i-1][j-1]+C[i-1][j];
}
}
二项式定理:
公式:
(x+y)n=C(n,0)∗x0∗yn+C(n,1)∗x1∗y(n−1)+...+C(n,n)∗xn∗y0
9.位运算:
(1)判断 n 是否为 2 的整数次幂: n&(n−1)==0;
(2)找到 n 中最低的 1 位:n&(~(n-1));
(3) a+b=a⨁b+2∗(a&b),异或是不进位的加法;
(4)或( ∣) 可以用来判断子集;
10.同余方程:
一元线性同余方程:
欧几里得算法:
gcd(a,b)=gcd(b,a%b)
拓展欧几里得:
方程 ax+by=gcd(a,b),必然有解。
证明:
当 b=0 时,有 gcd(a,b)=a, {x0=1y0=0 为一组解。
当 b!=0时,
a∗x1+b∗y1=gcd(a,b)=b∗x2+(a%b)∗y2
⇒a∗x1+b∗y1=b∗x2+(a−(a/b)∗b)∗y2
⇒a∗x1+b∗y1=a∗y2+b∗(x2−(a/b)∗y2)
所以: {x1=y2y1=x2−(a/b)∗y2
据此,一直向下递归,直到到达边界 b=0。然后回溯,利用不同方程的解的关系求解。
三种常见应用:
1.求解不定方程:
对于 ax+by=c 的不定方程,
{c%gcd(a,b)!=0,无整数解c%gcd(a,b)=0,有整数解
当 c%gcd(a,b)=0 时,
先求解方程 a∗x+b∗y=gcd(a,b),然后把解同时乘以 c/gcd(a,b),即可得出原方程的解。
假设 x0,y0 为方程的一组解,
则通解:
{x=x0+b/gcd(a,b)∗ky=y0−a/gcd(a,b)∗k
把通解代入原方程,可以发现恒成立。
x 的最小正整数解: x=(x%b+b)%b
注意:在一些问题中 a,b 等系数可能为负。
2.求解模线性方程(线性同余方程):
可以转化为求解不定方程。
3.求解模的逆元:
转化为求解不定方程。
拓展欧几里得解不定方程代码:
typedef long long ll;
ll exgcd(ll a,ll b,ll &x,ll &y)
{
if(b==0)
{
x=1;
y=0;
return a;
}
ll ans=exgcd(b,a%b,x,y);
ll tmp=x;
x=y;
y=tmp-(a/b)*y;
return ans;
}
ll cal(ll a,ll b,ll c)
{
ll x,y;
ll gcd=exgcd(a,b,x,y);
if(c%gcd)
return -1;
x=c/gcd*x;
x=(x%b+b)%b;
return x;
}
11.中国剩余定理:
一元线性同余方程组:
x=a1(mod m1)
x=a2(mod m2)
x=a3(mod m3)
x=a4(mod m4)
…
x=an(mod mn)
其中 m1,m2,...,mn 两两互质,对于任意的 a1,a2,...,an 方程组有解。
可以把解构造成如下形式:
x=p1+p2+p3+...+pn,
其中 p1%m1=a1 ,..., pn%mn=an,且 pi%∏j=1,j=inmj=0,
那么这样的 x 一定满足条件。
以求 p1 为例:
假设 tm=∏i=1,i=1nmi,
考虑 tm∗t=1(mod m1),
两边同时乘 a1,
有 tm∗t∗a1=a1(mod m1),那么 p1=tm∗t∗a1 就满足条件了。
因此关键在于求 t,可以发现 t 为 tm 的逆元,直接用拓展欧几里得即可求解。
模板代码:(FZU - 1402)
#include <cstdio>
#include <cstring>
using namespace std;
typedef long long ll;
ll m[12],aa[12];
int n;
ll exgcd(ll a,ll b,ll &x,ll &y)
{
if(b==0)
{
x=1;
y=0;
return a;
}
ll ans=exgcd(b,a%b,x,y);
ll tmp=x;
x=y;
y=tmp-(a/b)*y;
return ans;
}
ll crt()
{
ll M=1,ans=0;
for(int i=1;i<=n;i++)
M*=m[i];
for(int i=1;i<=n;i++)
{
ll tm=M/m[i];
ll x,y;
ll gcd=exgcd(tm,m[i],x,y);
ans=(ans+tm*aa[i]*x)%M;
}
ans=(ans%M+M)%M;//最小正整数解
return ans;
}
int main()
{
while(scanf("%d",&n)!=EOF)
{
for(int i=1;i<=n;i++)
scanf("%I64d%I64d",&m[i],&aa[i]);
printf("%I64d\n",crt());
}
return 0;
}
拓展CRT:
对于一般情况, m1,m2,m3,...,mn 不互质。
主要思路是:两两合并
分析:
首先,对于前两个方程:
{x=a1+m1∗x1x=a2+m2∗x2
合并有:
a1+m1∗x1=a2+m2∗x2
⇒m1∗x1+m2∗x2=(a2−a1)
利用拓展欧几里得即可解出该方程的最小的 x1 正整数解: x1′
代入原第一个方程中,就可以求解出满足第一个方程的最小正整数解 x′=x1′∗m1+a1
且该解同样满足方程 2,即 x′=a2+m2∗x2 ,但 x2 不必求出。
那么满足所有方程的解 x 的形式,一定满足 x=x′+k∗lcm(m1,m2)。
至此,我们就可以把原来的两个方程合并为一个方程:
x=x′+lcm(m1,m2)∗k
即把方程的解当作余数, lcm 当作模数,与其他方程形式相同。
以此类推,把所有方程合并,即可求出最后的解。
模板代码(luoguP4777):
#include <bits/stdc++.h>//最小非负整数x
using namespace std;
typedef long long ll;
const int N=1e5+5;
ll m[N],va[N];
ll mul(ll a,ll b,ll mod)
{
ll res=0;
while(b>0)
{
if(b&1) res=(res+a)%mod;
a=(a+a)%mod;
b>>=1;
}
return res;
}
ll exgcd(ll a,ll b,ll &x,ll &y)
{
if(b==0)
{
x=1,y=0;
return a;
}
ll gcd=exgcd(b,a%b,x,y);
ll tmp=x;
x=y;
y=tmp-(a/b)*y;
return gcd;
}
ll cal(ll a,ll b,ll c)
{
ll x=0,y=0;
ll gcd=exgcd(a,b,x,y);
if(c%gcd)
return -1;
b/=gcd;
x=mul(x,c/gcd,b);//注意溢出
x=(x%b+b)%b;
return x;
}
int main()
{
int n;
scanf("%d",&n);
for(int i=1;i<=n;i++)
scanf("%lld%lld",&m[i],&va[i]);
ll r=va[1],t=m[1];//x=r+t*x',为合并后的方程
for(int i=2;i<=n;i++)
{
ll x=cal(t,m[i],(va[i]-r)%m[i]+m[i]);//保证常数c为正,使用快速乘
//x=x*t+r;
//r=x;
r=x*t+r;
t=m[i]/__gcd(m[i],t)*t;
r%=t;
}
printf("%lld\n",(r%t+t)%t);
return 0;
}