写在前面
首先,感谢up主的视频
数论一般都要用到ll类型,就算没用到,你用ll类型计算也基本上不会多花时间,毕竟ACM考察的核心还是算法。
快速幂模板
int fpow(int a,int b,const int c)
{
ll base=a%c,ans=1;
while(b)
{
if(b&1)
ans = ans*base%c;
base = base*base%c;
b >>= 1;
}
return ans;
}
1. 整数的取余运算
定义: 带余除法
设 是整数,且
,则存在非负整数
,使得
且
,称
为商,
为余数
显然带余除法中的商和余数都是唯一的,在下文中将商记为 ,将余数记为
整数加减乘
注意,在计算减法时,通常需要加 ,防止变成负数
在计算乘法时,如果 较大(但不超过64位整数范围),可以使用快速乘法进行计算,原理与快速幂运算类似,复杂度为
ll fastMul(ll a, ll b, ll p){
a %= p;
ll ans = 0;
while(b > 0){
if(b&1)
ans = (ans+a)%p;
b >>= 1;
a = (a+a)%p;
}
return ans;
}
如果需要更快的快速乘法,可以使用 long double 数据类型进行计算,复杂度为
ll modMul(ll a, ll b, ll p){
if (p<=1000000000)
return a*b%p;
else if (p<=1000000000000ll)
return (((a*(b>>20)%p)<<20)+(a*(b&((1<<20)-1))))%p;
else
{
ll d = (ll)floor(a*(long double)b/p+0.5);
ll ret = (a*b-d*p)%p;
if (ret < 0)
ret += p;
return ret;
}
}
先抄着,作者说目前没有出错的情况,暂且相信他.
整数除法/求逆元
虽然取余运算对于“+”、“-”、“×”不难,但通常情况下
如何计算 ?我们有时可以找一个乘法逆元
,使得
,那么就有
如果 是素数,有下面的定理
费马小定理
设 是一个整数,
是一个素数,二者互质,那么
将上式改写一下,得到
因此取 即可,一般需要用快速幂计算,但要注意,与除数不能为0类似,要保证
ll inv(ll a,ll p)
{
return fpow(a,p-2);
}
上面的方法给出了模数为素数的解决方案,如果模数不是素数,可以用下面的方法
拓展欧几里得
相当于
只要存在逆元即可求,适用于个数不多但是mod很大的时候。
LL exgcd(LL a,LL b,LL &x,LL &y)
{
if(b==0)
{
x=1,y=0;
return a;
}
LL ret=exgcd(b,a%b,y,x);
y-=a/b*x;
return ret;
}
LL getInv(int a,int mod)
{
LL x,y;
LL d = exgcd(a, mod, x, y);
if(d == 1)
return (x%mod+mod)%mod;
return -1;//不存在逆元返回-1
}
只有 时才有解,否则逆元不存在
逆元不存在的情况,待补充,哔哩哔哩视频上有。
递推打逆元表
当p是个质数的时候有
证明:
设
于是有
移项得
于是
然后一直递归到1为止,因为1的逆元就是1
ll inv[N]={0,1};
void init()
{
for(int i = 2; i < N; i ++)
inv[i] = (MOD - MOD / i) * inv[MOD % i] % MOD;
}
2. 最大公因数与最小公倍数
2.1 最大公因数(GreatestCommonDivisor)
顾名思义,最大公因数就是公因数中最大的那个,我们记 的最大公因数为
,有如下性质
性质1:
性质2:
性质3:
视频里有个地方讲的不错,抄一下
性质4:
性质5:
ll gcd(ll a, ll b)
{
if(b==0)
return a;
return gcd(b,a%b);
}
2.2 最小公倍数(LowestCommonMultiple)
最小公倍数就是公倍数中最小的那个,我们记 的最小公倍数为
,有
ll lcm(ll a, ll b)
{
return a/gcd(a,b)*b;
}
注意是先除后乘,避免在中间过程中数据超出64位整数的范围
2.3 扩展欧几里得算法
考虑二元一次不定方程 ,其中
是已知的正整数,如何求出方程的解呢?
2.3.1 定理 上述方程有解的充要条件是 (
是
的倍数)
可以理解为, 是
可以表示出的最小正整数
2.3.2 定理 方程 的所有解为
其中 是一组特解
2.3.3 定理 方程 的所有解为
其中 是方程
的一组特解
ll ext_gcd(ll a,ll b,ll& x,ll& y)
{
ll d = a;
if (!b)
x = 1, y = 0;
else
{
d = ext_gcd(b,a%b,y,x);
y -= a/b*x;
}
return d;
}
代码未学习!!!
2.3.4 推论 逆元的存在性
存在 的充要条件是
3. 同余方程组
有方程就有方程组
其中 是整数,
是正整数
3.1 中国剩余定理(孙子定理)
设上述方程组中 两两互质,则上述方程组有解
设 是整数
的乘积
并设 是除了mi以外的n- 1个整数的乘积
设 为
模
意义下的逆元/数论倒数
方程组的通解形式为
上面的通解真的简单暴力, 注:
因为是通解
在模 的意义(应该就是结果对
取余)下,方程组只有一个解
ll CRT(int *m,int *a,int n)//m数组里面存的是模数,a里面存的是余数
{
ll x,y,res=0,mod=1,tmp;
for (int i=1;i<=n;++i)
mod*=m[i];
for (int i=1;i<=n;++i)
{
tmp=mod/m[i];
ext_gcd(m[i], tmp, x, y);
res=(res + y*tmp*a[i])%mod;
}
return (res+mod)%mod;//在这里管理一下正负号就OK了
}
3.2 拓展中国剩余定理
假设有两个方程
即
合并这两个方程,正负号随意啦!
我们可以用扩展欧几里得求出一个最小正整数解 ,令
就有新方程
所以,一路向下合并就OK了
代码解释
即
两边同除以 得
求exgcd时
方程 的一组解为
ll ex_CRT(ll *m,ll *a,int len)
{
ll lcm = 1;
ll ans = 0;
for (int i=0;i<len;i++)
{
ll k0,ki;
ll d = ext_gcd(lcm,m[i],k0,ki);
if ((a[i]-ans)%d!=0)//无解
return -1;
else
{
ll t = m[i]/d;
k0 = ( k0*(a[i]-ans)/d%t + t)%t;//加这个t为了保持正
ans = k0*lcm + ans;
lcm = lcm/d*m[i];
}
}
return ans;
}
4. 素数
素数是只有1和本身两个因数的数,1不是素数
可以理解为素数都有两个因子,而1只有一个因子
4.1 素数的判断
普法
bool isPrime(ll n){
if(n==1)
return false;
for(ll i=2;i*i<=n;i++)
if(n%i==0)return false;
return true;
}
这是最简单的素数的判断的参考代码模板,复杂度为
原理:对于一个大于1的整数,如果 是它的一个大于
的因子,那么
是它的小于
的因子
kn+i 法
这个我觉得只能判比较小的素数,感觉用处不大。
一个大于1的整数如果不是素数,那么一定有素因子,因此在枚举因子时只需要考虑可能为素数的因子即可。 法即枚举形如
的数,例如取
,那么
都不可能为素数,只需要枚举形如
的数即可,这样复杂度降低了
。
下面的模板是 法
的版本
bool isPrime(ll n){
if(n==2||n==3||n==5)
return 1;
if(n%2==0||n%3==0||n%5==0||n==1)
return 0;
ll c=7,a[8]={4,2,4,2,4,6,2,6};
while(c*c<=n)
{
for(auto i:a)
{
if(n%c==0)
return 0;
c += i;//c += i后应该是在遍历小于30的质数
}
}
return 1;
}
Miller-Rabin素性检测
如果 极大,可以使用Miller-Rabin素性检测法
未学习!!!
ll Rand(){
static ll x=(srand((int)time(0)),rand());
x+=1000003;
if(x>1000000007)x-=1000000007;
return x;
}
bool Witness(ll a,ll n){
ll t=0,u=n-1;
while(!(u&1))u>>=1,t++;
ll x=fpow(a,u,n),y;
while(t--){
y=x*x%n;
if(y==1 && x!=1 && x!=n-1)return true;
x=y;
}
return x!=1;
}
bool MillerRabin(ll n,ll s){
if(n==2||n==3||n==5)return 1;
if(n%2==0||n%3==0||n%5==0||n==1)return 0;
while(s--){
if(Witness(Rand()%(n-1)+1,n))return false;
}
return true;
}
当然, Rand() 怎么写可以自由发挥,这会影响其性能
4.2 素数筛
如果要求出不超过 的所有素数,素数筛是最好的选择,下面是一种朴素的筛法
埃氏筛
void getPrime(bool p[],int n)
{
for(int i=1;i<=n;i++)
p[i]=true;
p[1]=false;
for(int i=2;i<=n;i++)
{
if(p[i])
{
for(int j=i+i;j<=n;j+=i)
p[j]=false;
}
}
}
这种方法的原理是从小到大将素数的倍数筛掉,复杂度为 ,注意到每个合数如果有多个素因子,那么就会被重复筛掉,造成复杂度的浪费,因此,用下面的方法可以保证每个合数只被它最小的素因子筛掉一遍,以
的复杂度解决上述问题
线性筛
ll getPrime(ll n,bool vis[],ll prime[])
{
ll tot=0;
for(ll i=1;i<=n;i++)
vis[i]=0;
for(ll i=2;i<=n;i++)
{
if(!vis[i])
prime[tot++]=i;
for(ll j=0;j<tot;j++)
{
if(prime[j]*i>n)
break;
vis[prime[j]*i]=1;
if(i%prime[j]==0)
break;
}
}
return tot;
}
分析:
i为质数时,筛出的数都是 的形式,
之间不相等(除了
)
i为合数时,只能筛出不大于 的最小质因子
的质数
一句话解释:每个合数必有一个最大因子(不包括它本身) ,用这个因子把合数筛掉
那又怎么保证都筛干净? 好像就已经保证了....