基础数论

  1. 整除
  2. GCD&LCM
  3. 拓展欧几里得算法
  4. 模运算
  5. 同余
  6. 同余式(一次同余式)
  7. 逆元
  8. 素数筛
  9. 唯一分解定理
  10. 欧拉函数
  11. 中国剩余定理

整除

a%b==0

可以写作:b|a

带余数除法

​ 对任意整数a,b>0,存在唯一的数对q,r,使得a=bq+r(0<=r<|b|)成立,且q,r是唯一的。b是商,这里的余数 r 称为最小非负余数。

负数取余

余数的性质:

任一整数被正整数 a 除后,余数一定是且仅是 0 到 a-1 这 a 个数中的一个。

相邻的 a 个整数被正整数 a 除后,恰好取到上述 a 个余数。特别地,一定有且仅有一个数被 a 整除。

性质:

  1. 若a|b < - > -a|b < - > a|-b < - >|a|||b|
  2. 若a|b,b|c ->a|c
  3. 若a|b,a|c -> a|(bx+cy) 其中x,y为任意整数
  4. 若a|b -> am|bm 其中m为非零整数
  5. 若a|b,b|a -> b=±a <-> |b|=|a|
  6. 若a|bc,且a与c互质,则a|b
  7. 若a|b,a|c,且b与c互质,则a|bc
  8. 若a|b,c为任意整数,则b|ac

GCD&LCM

最大公因数,gcd(a,b)常记为(a,b)

最小公倍数,lcm(a,b)=a*b/gcd(a,b);常记为[a,b]

辗转相除法:gcd(a,b)=gcd(b,a%b)

更相减损术:gcd(a,b)=gcd(b,a-b)

d|a,d|b 等价于 d|(a-b),d|b

证:

​ a=d*q1 ,b=d*q2

​ a-b=d*(q1-q2)

​ 所以d|(a-b)

int gcd_1(int a,int b){
   
	return a%b==0?b:gcd_1(b,a%b);
}
int gcd_2(int a,int b){
   
	return b==0?a:gcd_2(b,a%b);
}
int gcd(int a,int b){
   
	while(a!=b){
   
		if(a>b) a-=b;
		else b-=a;
	}
	return a;
}

扩展欧几里得算法

裴蜀定理:设a,b是不全为0的整数,则存在整数x,y,使得ax+by=gcd(a,b);

//二元一次方程组ax+by=c 有解的充要条件为gcd(a,b)|c

int exgcd(int a,int b,int &x,int &y){
   
	if(b==0) {
   
		x=1;
		y=0;
		return a;
	}
	int d=exgcd(b,a%b,x,y);
	int temp=x;
	x=y;
	y=temp-a/b*y;
	return d;
}

void EX_gcd(int a, int b, int &x, int &y)
{
   
    if(b == 0)//递归出口
    {
   
        x = 1;
        y = 0;
        return;
    }
    int x1, y1;
    Ex_gcd(b, a%b, x1, y1);
    x = y1;
    y = x1-(a/b)*y1;
}

应用:

  1. 方程ax+by=c的求解
  2. 同余式的求解
  3. 逆元的求解以及除法取模的计算

模运算

性质:

1.取模运算:a%p(a mod p),表示a除以p的余数

2.运算:

​ (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

​ //除法的取模需要逆元,不能进行类似操作。

​ //模运算满足结合律、交换律、分配律

3.a≡b(mod n)表示a和b模n同余,即ab除以n的余数相等

-5 mod 3 = -2 :这是因为在a,p异号的时候,c++遵循使商尽可能大的原则,选取了-1作为商。所以我们平时计算减法时,为防止要取mod的数字为负数,多加上一个p:

(a - b ) % p = ( ( a % p ) - ( b % p ) + p ) % p;


同余

a%b==c%b 可以说a≡c(mod b);a,c对mod b同余

第二定义:若 m | ( a-b ),a,b对mod m同余

性质:

(1)若a≡b(mod m),则(a,m)=(b,m) [欧几里得算法]

(2)若a≡b(mod m),则ak≡bk(mod m)

(3)若a≡b(mod m),且a=a|d,b=b|d, (d,m)=1,则ad≡bd(mod m);


同余式

若用f(x)表示系数为整数的多项式,又设m是一个正整数,则f(x)≡0(mod m)叫做模m的同余式。

//ax+by=gcd(a,b)

一次同余式:ax≡b(mod m) 使同余式成立的x叫做该式的一个解

ax≡b(mod m) 与ax+my=b等价

定理1:一次同余式(又叫线性同余方程)有解的充要条件是 (a,m)|b。

证:

​ 一次同余式可写成:ax=my+b => ax-my=b

​ 因y无关紧要,令y=-y,得ax+my=b => (a,m)|b [裴蜀定理];

​ 证毕;

定理2:若gcd(a,b)=1 ,且 x0、y0 为方程 ax+by=c 的一组解,则该方程的任意解可表示为:x=x0+bt,y=y0-at, 且对任意整数 t 都成立。

根据定理 2,可以求出方程的所有解。但在实际问题中,我们往往被要求求出一个最小整数解,也就是一个特解x=(x mod t + t ) mod t ,其中 t=( b / gcd(a,b))


逆元(Day 1 )

方程ax≡1(mod m)的一个解x,称为x为a模m的逆。

//(a,m)=1;

设k是b的逆元 <=> bk mod m = 1;

逆元的求法

(1)拓展欧几里得解方程

exgcd是求解不定方程ax+by=c的解,由逆元定义,a*inv(a)≡1(mod m)

即a* inv(a)+m*y=1的方程的解,由于a,m已知,根据exgcd可求出inv(a)

只有gcd(a,m)=1时,才有逆元

int exgcd(int a,int b,int &x,int &y){
   
	if(b==0) {
   
		x=1;
		y=0;
		return a;
	}
	int d=exgcd(b,a%b,x,y);
	int temp=x;
	x=y;
	y=temp-a/b*y;
	return d;
}
int getinv(int a,int mod){
   //a在mod下的逆元 
	int x,y;
	int d=exgcd(a,mod,x,y);
	return d==1?(x%mod+mod)%mod:-1; 
}

(2)费马小定理:p是素数,gcd(a,p)=1

	int quickpow(int a,int p,int mod){
   //快速幂 
		int t=1,tt=a%mod;
		while(p){
   
			if(p&1) t=t*tt%mod;
			tt=tt*tt%mod;
			p>>=1;
		}
		return t;
	}
	int getInv(int a,int mod){
   
		return quick(a,mod-2,mod);
	} 

线性逆元
线性求1~n所有数关于p的逆元

typedef long long ll;
const int N=1e5+10;
ll inv[N],mod;

void init(int n){
   
	inv[1]=1;
	for(int i=2;i<=n;i++){
   
		inv[i] = ( mod - (mod/i)) * inv[mod % i] % mod;
		//线性递推
	}
}

素数筛

**素数记数函数:**小于等于x的素数的个数,用F(x)表示,随着x的增大,有这样近似的结果:

F(x) ~ x / ln(x)

利用素数来解决问题,如何求出一定范围内的所有素数

p3912 素数个数

int pri[maxn],cnt=0;
void getprime(int n){
   //暴力求法 
	for(int i=2;i<=n;i++){
   
		int flag=1;
		for(int j=2;j<i;j++){
   
			if(i%j==0){
   flag=0;break;}
		}
		if(flag) pri[cnt++]=i;
	}
} 

埃氏筛法

思想:素数的倍数一定不是素数

局限:每个合数会被多次筛去:24会被2,3筛去

const int maxn=100; 
bool isprime[maxn];
int pri[maxn],cnt=0;
void getprime(int n){
   
	for(int i=1;i<=n;i++) isprime[i]=1;
	isprime[0]=isprime[1]=0;	//初始化
	for(int i=2;i<=n;i++){
   		//枚举n
		if(isprime[i]){
   		    //判断是否为素数的倍数
			for(int j=i*i;j<=n;j+=i){
   
				isprime[j]=0;	//更改标记
			}
		}
	}
	for(int i=2;i<=n;i++){
   
		if(isprime[i])
			pri[cnt++]=i;
	}
} 

线性筛法(欧拉筛):一种可以保证所有数只被筛去一次的筛法,时间复杂度为O(n)。

欧拉筛的难点就在于对if (i % prime[j] == 0)这步的理解,当i是prime[j]的整数倍时,记 m = i / prime[j],那么 i * prime[j+1] 就可以变为 (m * prime[j+1]) * prime[j],这说明 i * prime[j+1] 是 prime[j] 的整数倍,不需要再进行标记(在之后会被 prime[j] * 某个数 标记),对于 prime[j+2] 及之后的素数同理,直接跳出循环,这样就避免了重复标记。

bool judge[maxn];//判断是否为合数
const int maxn=100; 
int pri[maxn],cnt=0;
void getprime(int n){
   
      for(int i = 2;i<=n;i++){
   
      	   if(!judge[i])
		        pri[cnt++] = i;		   //记录素数
      	   for(int j = 0;j<cnt;j++){
   
              	if(i*pri[j]>n)break;	//判断是否越界
        	    judge[i*pri[j]] = 1;	//标记合数
        	    if(i%pri[j]==0)break;	//关键:当合数进入循环就退出,内层循环只进行一次,减少时间复杂度
            }
       }
}

区间素数筛
P1835 素数密度

typedef long long ll;
const int maxn = 1000005;
bool is_prime[maxn];		//偏移后判断素数的数组,脚标从0开始 
bool is_prime_small[maxn];	//筛选[2,sqrt(b))中所有的素数 
ll prime[maxn];	//记录区间中的素数 
ll prime_num=0;	//区间素数下标 
 
//对区间[a,b)内的整数执行筛法,is_prime[i-a]=true --- 表示i是素数 注意这里下标偏移了a,所以从0开始。
void segment_sieve(ll a,ll b) {
   
    for(ll i=0;i*i<b;++i) is_prime_small[i]=true; //对[2,sqrt(b))的初始化全为质数
    for(ll i=0;i<b-a;++i) is_prime[i]=true; //对下标偏移后的[a,b)进行初始化
 
    for(ll i=2;i*i<b;++i) {
   
        if(is_prime_small[i]) {
   
            for(ll j=2*i;j*j<b;j+=i) is_prime_small[j]=false;  //筛选[2,sqrt(b));
            //(a+i-1)/i得到最接近a的i的倍数,最低是i的2倍,然后筛选
            for(ll j=max(2LL,(a+i-1)/i)*i;j<b;j+=i) is_prime[j-a]=false;
        }
    }
    for(ll i=0;i<b-a;++i)  //统计个数,prime_num 即为[a,b)区间素数个数,prime[]存储素数是哪些
        if(is_prime[i]) prime[prime_num++]=i+a;
}

唯一分解定理

算术基本定理):任意大于1的整数可以表示成质数的乘积。

判断数字n的因子有哪些,只需要枚举到sqrt(n)即可

判断n范围内哪些数字能够成为因子,只需枚举到n/2即可

//唯一分解定理
const int maxn = 10000 + 7;
int prime[maxn],len,n,num;	//prime:质数 
int p[maxn],q[maxn];		//p:因子 q:幂次 
bool judge[maxn];
void isPrime(){
   //素数打表
   len = 0;
   memset(judge,0,sizeof(judge));
   judge[1] = 1;
   for(int i = 2;i<=maxn;i++){
   
      	if(!judge[i])
			prime[len++] = i;
      	for(int j = 0;j<len;j++){
   
        	if(i*prime[j]>maxn)break;
        	judge[i*prime[j]] = 1;	//标记不合法 
        	if(i%prime[j]==0)break;
      }
   }
}
void Split(int k){
   //分解
    memset(p,0,sizeof(p));
    memset(q,0,sizeof(q));
   	for(int i = 0;i<len;i++){
   
      	while(k%prime[i]==0){
   				//能被分解
         	if(!p[num])p[num] = prime[i];	 
         	q[num]++;
        	k/=prime[i];
     	 }
      	if(p[num])num++;
      	if(k==1)break;//退出条件是变为1
   }
}

int main()
{
   
    isPrime();
    while(scanf("%d",&n)!=EOF){
   
        num = 0;
        Split(n);
        printf("%d = ",n);
        for(int i = 0;i<num;i++){
   
            if(i!=0)printf("*");
            printf("%d^%d",p[i],q[i]);
        }
        printf("\n");
    }
    return 0;
}

优化:
根据求因子条件,素数只枚举到prime[i]*prime[i]<=n , 最后若n>1就说明还剩下一个一次方的素数。

原因:

若n%prime[i]==0,则n = prime[i] * q , 然后我们把n里面的prime[i]因子除尽以后,n = 1* k , n就变成了一个新的数字,因为我们的素数枚举是从小到大的,小的除尽了,如果k是一个合数,那么肯定能拆成素数乘积,而且这个素数是肯定>prime[i]的,也一定满足prime[j]prime[j] <=n是能继续拆的;另一方面,如果只剩下一个素数,就不会满足prime[j]*prime[j]<=n了,所以这种方法是可行的。

//优化:
//根据求因子条件,素数只枚举到prime[i]*prime[i]<=n , 最后若n>1就说明还剩下一个一次方的素数
void Split(int k){
   
    memset(p,0,sizeof(p));
    memset(q,0,sizeof(q));
   for(int i = 0;i<len&&prime[i]*prime[i]<=k;i++){
   //判断条件变化
      while(k%prime[i]==0){
   
         if(!p[num])p[num] = prime[i];
         q[num]++;
         k/=prime[i];
      }
      if(p[num])num++;
   }
   if(k>1){
   p[num] = k;q[num++]++;}//结尾判断
}
//直接分解法(不用素数打表)

void Split(int k){
   
    memset(p,0,sizeof(p));
    memset(q,0,sizeof(q));
   for(int i = 2;i*i<=k;i++){
   
      while(k%i==0){
   
        if(!p[num])p[num] = i;
        q[num]++;
        k/=i;
      }
      if(p[num])num++;
   }
   if(k>1){
   p[num] = k;q[num++]++;}
}

应用:

欧拉函数求互质


欧拉函数

欧拉公式的延伸:一个数的所有质因子之和是 euler(n)*n/2;

//欧拉函数,在分解质因数的同时求得欧拉函数
//①直接求小于或等于n,且与n互质的数个数:
inline int euler_one(int n)
{
   
    int ans = n;
    for(int i = 2;  i * i <= n; ++ i){
   
        if(n % i == 0){
   					//合数
            ans = ans / i * (i - 1);	 //公式
            while(n % i == 0)n /= i;	 //n除因子改变n
        }
    }
    if(n > 1)ans = ans / n * (n - 1);	//还能分解一个质因子
    return ans;
}

//②求[1,n]之间每个数的质因数的个数
首先,要明确一些性质,下面三条性质我们会在欧拉函数线性筛中使用到。
1.如果p为素数,则φ(p)=p-12.若a为n的质因子,且(n%a==0&&(n/a)%a==0),即如果a是n的多重质因子,则φ(n)=φ(n/a)*a;
3.若a为n的质因子,但(n%a==0&&(n/a)%a! =0),即如果a是n的单质因子,则φ(n)=φ(n/a)*(a-1)//线性筛欧拉函数建立在线性筛素数的基础之上

void getPhi()
{
   
	phi[1] = 1;//存欧拉函数值
	cnt = 0;
	for(int i = 1; i < MAXN; i++)
		isprime[i] = 1;
 
	for(int i = 2; i < MAXN; i++)
	{
   
		if(isprime[i])
		{
   
			prime[cnt++] = i;	//记录素数
			phi[i] = i - 1;		//记录欧拉函数值 性质1
		}
		for(int j = 0; j<cnt&&prime[j]*i<MAXN; j ++) //素数筛
		{
   
			isprime[i*prime[j]] = 0;	//合数
			if(i%prime[j] == 0)
			{
   
				phi[i*prime[j]] = phi[i] * (prime[j]);
                 //如果a是n的多重质因子,则φ(n)=φ(n/a)*a
				break;
			}
			else
			{
   
				phi[i*prime[j]] = phi[i]*( prime[j] - 1 );
                 //即如果a是n的单质因子,则φ(n)=φ(n/a)*(a-1)
			}
		}
	}
}


//如何求一个数约数的个数
void getDiv()
{
   
	for(int i = 1; i < MAXN; i++)
		isprime[i] = 1;
	for(int i = 2; i < MAXN; i++)
	{
   
		if(isprime[i])
		{
   
			prime[cnt++] = i;
			minp[i] = 1; //质数的最小质因子为其本身,数量为1
			divnum[i]= 2;//质数的约数为1和它本身
		}
		for(int j = 0; j < cnt&&i*prime[j] < MAXN; j++) //线性筛
		{
   
			isprime[i*prime[j]] = 0;
			if(i%prime[j] == 0)  //同样按照最小质因子*剩余因子积的策略
			{
   
				divnum[i*prime[j]] = divnum[i] / (minp[i] + 1)*(minp[i] + 2);	//根据公式进行更新
				minp[i*prime[j]] = minp[i] + 1; //最小质因子数多一个
				break;
			}
			else //如果之前没有最小质因子,说明prime[j]为最小质因子,且数量为1
			{
   
				divnum[i*prime[j]] = divnum[i] * divnum[prime[j]];	//约数函数的可乘性
				minp[i*prime[j]] = 1;
			}
		}
	}
}

中国剩余定理

解决模数互质的同余方程

在《孙子算经》中有这样一个问题:“今有物不知其数,三三数之剩二(除以3余2),五五数之剩三(除以5余3),七七数之剩二(除以7余2),问物几何?”这个问题称为“孙子问题”,该问题的一般解法国际上称为“中国剩余定理”。

具体解法分三步:

  • 找到三个数:从3和5的公倍数中找出被7除余2的最小数30,从3和7的公倍数中找出被5除余3 的最小数63,最后从5和7的公倍数中找出除3余1的最小数140。
  • 然后把这三个数相加得到30 + 63 + 140 = 233满足以上所有条件。
  • 233满足以上所有条件,但是并不是最小的解, 用233除以3,5,7三个数的最小公倍数105,得到余数23,即233%105=23。这个余数23就是符合条件的最小数。

//洛谷P3868
#include<bits/stdc++.h>
using namespace std;
#define LL long long
#define ld long double
#define maxn 1000
LL a[maxn],b[maxn];
void EX_gcd(LL a, LL b, LL &x, LL &y)
{
   
    if(b == 0)//递归出口
    {
   
        x = 1;
        y = 0;
        return;
    }
    LL t;
    EX_gcd(b, a%b, x, y);
    t = x;
    x = y;
    y = t-(a/b)*y;
}

LL mod_mul(LL a,LL b,LL mod){
   //快速积取模
	LL sum=0;
	while(b){
   
		if(b&1) sum=(sum+a)%mod;
		a=(a+a)%mod;
		b>>=1;
	}
	return sum;
}

LL q_mod(LL x, LL y, LL p) // ll 代表 long long, ld 代表long double 
{
   
	x %= p;
	y %= p;
	LL z = (ld) x * y / p;
	LL ans = x * y - z * p;
	if(ans < 0)
		ans += p;
	else if(ans >= p)
		ans -= p;
	 
	return ans;
}


LL CRT(LL m[], LL a[], int n){
     // ans % m = a
    LL M = 1, ans = 0;
    for(int i=0; i<n; i++) M *= m[i];//计算 mod
    for(int i=0; i<n; i++){
   
        LL x, y;
        LL Mi = M / m[i];			//Mi:当前式子中要取的模
        EX_gcd(Mi, m[i], x, y);       //拓展欧几里得算法
        //ans = (ans + Mi*x*a[i]) % M;
        //ans = (ans + mod_mul(mod_mul(Mi, x, M), a[i], M)) % M;
        ans = (ans + q_mod(q_mod(Mi, x, M), a[i], M)) % M;
    }
    if(ans < 0) ans += M;  //最小非负整数
    return ans;
}
int main(){
   
	LL n;
	cin>>n;
	for(LL i=0;i<n;i++){
   
		cin>>b[i];
	}
	for(LL i=0;i<n;i++){
   
		cin>>a[i];
	}
	cout<<CRT(a,b,n);
	return 0;

} 

解决模数可不互质的同余方程组

扩展中国剩余定理

LL EX_gcd(LL a, LL b, LL &x, LL &y){
   
	if(b==0) {
   
		x=1;
		y=0;
		return a;
	}
	LL d=EX_gcd(b,a%b,x,y);
	LL temp=x;
	x=y;
	y=temp-a/b*y;
	return d;
}

LL EX_CRT(LL m[], LL a[], int n) {
    // ans % m = a
    LL M = m[0], ans = a[0];
    for(int i=1; i<n; i++)
    {
   
        LL aa = M, bb = m[i], cc = (a[i] - ans % bb + bb) % bb;//aa * x ≡ cc( mod bb)
        LL x = 0, y = 0;
        LL d = EX_gcd(aa, bb, x, y);	
        bb = bb / d;
        if(cc % d) return -1; // 不存在
        x=((x * cc / d) % bb + bb) % bb;//非负整数解
        ans += M * x;//更新前k个方程组的答案
        M *= bb;	// M 为前 k 个 m 的lcm
        ans = (ans % M + M) % M;
    }
    return ans;
}