数论专题
数论专题
编者:李昕烨、刘子闻、庄德楷
整理:刘子闻、李昕烨
资料来源:网络、集训课程
截稿:2020.9.18 于机房
前言:总结一下之前学的数论内容
1.素数与唯一分解定理
(0) 素数
显然大于1的正整数
0既不是合数也不是素数。
(1) 素数计数函数
对于小于或等于
(2) 唯一分解定理
唯一分解定理又称为算数基本定理,基本内容是:
每个大于1的自然数,要么本身就是质数,要么可以写为2个或以上的质数的积,而且这些质因子按大小排列之后,写法仅有一种方式。
另一种方法表示就是:
对于任何一个大于1的正整数,都存在一个标准的分解式
此定理表明:任何一个大于 1 的正整数都可以表示为素数的积。
(3) 质因数分解
void func(int x,vector<int>& r){
r.clear();
for(int i=2;i<=sqrt(x);i++){
if(x%i==0){
while(x%i==0){
x/=i;
r.push_back(i);
}
}
}
if(x!=1) r.push_back(x);
}
//O(sqrt(x))
void func(int x,vector<int>& r){
r.clear();
for(int i=2;i<=sqrt(x);i++){
if(x%i==0){
while(x%i==0){
x/=i;
r.push_back(i);
}
if(isprime(x)) break;
}
}
if(x!=1) r.push_back(x);
}
//一个简单优化
(4) 素数判断:试除法
暴力做法自然可以枚举从小到大的每个数看是否能整除
但很容易发现这样一个事实:如果
这个结论告诉我们,对于每一对
由于1肯定是约数,所以不检验它。
bool isPrime(a) {
if (a < 2) return 0;
for (int i = 2; i * i <= a; ++i)
if (a % i == 0) return 0;
return 1;
}
(5) 伯特兰—切比雪夫定理
伯特兰—切比雪夫定理说明:若整数
n > 3 ,则至少存在一个质数p ,符合n < p < 2n − 2 。 另一个稍弱说法是:对于所有大于1 的整数n ,至少存在一个质数p ,符合n < p < 2n 。 其中两个加强结果(定理): 定理 1: 对任意自然数n > 6 , 至少存在一个4k + 1 型和一个4k + 3 型素数p 使得n < p < 2n 。 定理 2: 对任意自然数k , 存在自然数N , 对任意自然数n > N 至少存在k 个素数p 使得n < p < 2n 。
相关例题:洛谷P5535 小道消息
2.素数筛法
(1) Eratosthenes 筛法 (埃拉托斯特尼筛法)
时间复杂度是
int Eratosthenes(int n)
{
int p = 0;
for (int i = 0; i <= n; ++i) is_prime[i] = 1;
is_prime[0] = is_prime[1] = 0;
for (int i = 2; i <= n; ++i) {
if (is_prime[i]) {
prime[p++] = i; // prime[p]是i,后置自增运算代表当前素数数量
if ((long long)i * i <= n)
for (int j = i * i; j <= n; j += i)
// 因为从 2 到 i - 1 的倍数我们之前筛过了,这里直接从 i
// 的倍数开始,提高了运行速度
is_prime[j] = 0; // 是i的倍数的均不是素数
}
}
return p;
}
(2) Euler 筛法 (欧拉筛法、线性筛法)
时间复杂度
洛谷P3383 线性筛模板
代码中,外层枚举
内层从小到大枚举
注意:
则有
关于求最小公倍数(lcm)
不难验证
那么
由于
正确的写法一般是先除后乘法,即为
4.拓展欧几里得算法(exgcd)
洛谷P5656
用于求解形如
当
此时
(实际上此时y大小不影响代码实现)
当
可以求得
所以得到了递归结束后
证明如下:
代码:
void exgcd(int a,int b,int &x,int &y)
{
if(!b)
{
x=1;
y=0;
return;
}
exgcd(b,a%b,x,y);
int p;
p=x;
x=y;
y=p-(a/b)*y;//x=y',y=x'-(a/b)*y'
return;
}
还有一种更短的
void exgcd(int a,int b,int &x,int &y)
{
if(!b)
{
x=1;y=0;
return;
}
exgcd(b,a%b,y,x)//x=y',y=x'
y-=(a/b)*x;//y=x'-(a/b)*y'
return;
}
根据递归,我们可以知道这个x,y特解满足
|x|+|y|最小
但是我们不满足求这一组解
如果设
那么:
所以x,y的解可以写成
在此时,我们可以求出x,y最小非负整数解
分别是
xin=(x%b+b)%b;//最小非负整数解
yin=(y%a+a)%a;//最小非负整数解
xin=x>0&&x%b!=0?x%b:x%b+b;//最小正整数解
yin=y>0&&y%a!=0?y%a:y%a+a;//最小正整数解
//最大整数解可以通过代入求出
当然,我们看到上面的求证过程中一直没有出现用到
“
那么我们可以推广:
设a,b,c为任意整数,若方程
则它的任意整数解可以写成
由此我们知道了任意整数解的求法,那
这里给出了一般性的做法,但为了编写代码方便
我们一般这么做
设
则
此时
我们便求得了
这里给出p5656的代码
//exgcd
#include <bits/stdc++.h>
#define int long long
using namespace std;
int gcd(int a,int b)
{
if(b==0) return a;
else return gcd(b,a%b);
}
void exgcd(int a,int b,int &x,int &y)
{
if(!b)
{
x=1;
y=0;
return;
}
exgcd(b,a%b,x,y);
int p;
p=x;
x=y;
y=p-(a/b)*y;//x=y',y=x'-(a/b)*y'
return;
}
signed main()
{
int t;
scanf("%lld",&t);
while(t--)
{
int a,b,c,x=0,y=0,xin,xax,yin,yax,npa=0,g;//分别是x,y最小,最大正整数解 ,和正整数解的数量
scanf("%lld%lld%lld",&a,&b,&c);
g=gcd(a,b);
if(c%g!=0) printf("-1\n");//裴蜀定理
else
{
a/=g;
b/=g;
c/=g;//eg:求6x+15y=15:a:6/3=2,b:15/3=5,c:15/3=5
//求2x'+5y'=1的一组解,x'=-2,y'=1
//则原解为x'*c,x=-10,y=5;
exgcd(a,b,x,y);//a'x+b'y=1
x*=c;
y*=c;
//xin=(x%b+b)%b;最小非负整数解
xin=x>0&&x%b!=0?x%b:x%b+b;
yax=(c-a*xin)/b;
//yin=(y%a+a)%a;最小非负整数解
yin=y>0&&y%a!=0?y%a:y%a+a;
xax=(c-b*yin)/a;
if(xax>0)//yax>0也行
npa=(xax-xin)/b+1;//正整数解数量
//npa=(yax-yin)/a+1;
if(npa==0)
{
printf("%lld %lld\n",xin,yin);
}
else printf("%lld %lld %lld %lld %lld\n",npa,xin,yin,xax,yax);
}
}
return 0;
}
例题:POJ2142 The Balance
题意简述:
有一天平和两种数量无限的砝码(重为a和b),天平左右都可以放砝码,称质量为c 的物品,要求:放置的砝码数量尽量少;当砝码数量相同时,总质量尽量小。
题解:
本质就是
不难想到可能满足第一条的只有
代码:
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
#define ll long long
ll exgcd(ll a,ll b,ll &x,ll &y)
{
if(!b)
{
x = 1;
y = 0;
return a;
}
ll gcd = exgcd(b,a%b,y,x);
y -= (a/b)*x;
return gcd;
}
int main()
{
ll m,n,k;
while(cin>>m>>n>>k&& m+n+k)
{
ll x,y;
ll vx,vy;
ll gcd = exgcd(m,n,x,y);
ll ng = n / gcd;
ll mg = m / gcd;
vx = x* k / gcd;
vx = (vx%ng+ng)%ng;
vy = (k - m*vx) / n;
if(vy < 0) vy = -vy;
y = y*k/gcd;
y = (y%mg+mg)%mg;
x = (k - n*y) / m;
if(x < 0) x = -x;
if(x+y > vx+vy)
{
x = vx;
y = vy;
}
cout<<x<<" "<<y<<endl;
}
return 0;
}
5.线性同余方程
对于形如
注意:
又因为它与
例题:
洛谷P1082
转换成
转换成移项可得
之后用exgcd求解
代码:
//转化为求解ax+by=1
#include <bits/stdc++.h>
using namespace std;
void exgcd(int a,int b,int &x,int &y)
{
if(!b)
{
x=1;y=0;
return;
}
exgcd(b,a%b,x,y);
int temp=x;
x=y;
y=temp-(a/b)*y;
}
int main()
{
int a,b,x,y;
scanf("%d%d",&a,&b);
exgcd(a,b,x,y);
x=x>0&&x%b!=0?x%b:x%b+b;
printf("%d",x);
return 0;
}
拓展性质:
那么我们设
这个式子便可写作:
用exgcd求解就行,就是别忘了最开始要把S,W都先通过模运算变成正值。
代码:
#include <bits/stdc++.h>
#define ll long long
using namespace std;
ll x,y,m,n,l;
ll xx,yy;
ll exgcd(ll a,ll b,ll &xx,ll &yy)
{
if(!b)
{
xx = 1;
yy = 0;
return a;
}
ll gcd = exgcd(b,a%b,yy,xx);
yy -= (a/b)*xx;
return gcd;
}
int main()
{
scanf("%lld%lld%lld%lld%lld",&x,&y,&m,&n,&l);
ll aa = ((m-n)%l+l)%l;
ll bb = l;
ll cc = ((y-x)%l +l)%l;
ll gcd = exgcd(aa,bb,xx,yy);
ll bg = bb / gcd;
if(cc % gcd !=0) cout<<"Impossible";
else
{
ll anss = ((xx*(cc/gcd))%bg+bg)%bg;
cout<<anss;
}
return 0;
}
例题:POJ2115 C Looooops
题意简述:
for(i=A;i!=B;i+=C){i%=1<<k;}
问会循环多少次
有可能死循环,输出FOREVER
题解:
转化为
那么就是
就可以求解了。
#include <cstdio>
#include <iostream>
#include <algorithm>
#include <cmath>
using namespace std;
#define ll long long
ll exgcd(ll a,ll b,ll &x,ll &y)
{
if(!b)
{
x = 1;
y = 0;
return a;
}
ll gcd = exgcd(b,a%b,y,x);
y -= (a/b)*x;
return gcd;
}
int main()
{
ll A,B,C,k;
while(scanf("%lld%lld%lld%lld",&A,&B,&C,&k),(A+B+C+k))//这个逗号表达式代表算两个表达式,返回后面的值
{
ll a,b,c,x,y;
// if(!c) {cout<<"0"<<endl; continue;}
a = C;
b = 1ll<<k;//注意1ll
c = ((B - A)%b+b)%b;
ll gcd = exgcd(a,b,x,y);
if(c % gcd) {cout<<"FOREVER"<<endl; continue;}
ll bg = b / gcd;
x = ((x * c/gcd) %bg + bg)%bg;
cout<<x<<endl;
}
return 0;
}
6.乘法逆元
乘法逆元,一般用于求
(1) exgcd
模数可以 不为质数
这个方法十分容易理解,而且对于单个查找效率似乎也还不错(尤其对于
这个就是利用拓欧求解 线性同余方程
求解这个方程的解。
而且这个做法还有个好处在于,当
代码比较简单:
void Exgcd(ll a, ll b, ll &x, ll &y) {
if (!b) x = 1, y = 0;
else Exgcd(b, a % b, y, x), y -= a / b * x;
}
int main() {
ll x, y;
Exgcd (a, p, x, y);
x = (x % p + p) % p;
printf ("%d\n", x); //x是a在mod p下的逆元
}
(2) 费马小定理
只适用于模数为质数的情况
若
p 为素数,a 为正整数,且a 、p 互质。 则有a^{p-1} \equiv 1 (\bmod {p}) 。
另一个形式:
对于任意整数
a ,有a^p \equiv \ a (mod \ p)
观察第一个公式:
这个我们就可以发现它这个式子右边刚好为 1 。
所以我们就可以放入原式,就可以得到:
所以我们可以用快速幂来算出
ll fpm(ll x, ll power, ll mod) {
x %= mod;
ll ans = 1;
for (; power; power >>= 1, (x *= x) %= mod)
if(power & 1) (ans *= x) %= mod;
return ans;
}
(3) 线性算法
只适用于模数为质数的情况
用于求一连串数字对于一个
洛谷P3811
只能用这种方法,别的算法都比这些要求一串要慢。
首先我们有一个,
然后设
再将这个式子放到
然后乘上
于是,我们就可以从前面推出当前的逆元了。
注意:
#include <bits/stdc++.h>
#define ll long long
#define N 3000010
using namespace std;
ll inv[N];
int main()
{
int n,p;
scanf("%d%d",&n,&p);
inv[1]=1;
printf("1\n");
for(int i=2;i<=n;i++)
{
inv[i]=(ll)(p-p/i)*inv[p%i]%p;
printf("%lld\n",inv[i]);
}
return 0;
}
(4) 阶乘逆元法
只适用于模数为质数的情况
设
则:
-
证明:
f(i-1)=\frac{1}{\ (i-1)\ !}=\frac{1}{i\ !}\times i =f(i)\times i
假设要求
先求得
再用费马小定理 求得
之后递推出
但是
需要继续转化。
可知 :
-
证明 :
inv(i)=\frac{1}{i}=\frac{1}{i\ !}\times (i-1)\ ! = inv(i!)\times (i-1)!
按照上述方法转换, 可得:
即得答案 。
#include<cstdio>
#define ll long long
using namespace std;
ll mul(ll a,ll b,ll mod) //快速幂模板
{
ll ans=1;
while(b)
{
if(b&1) ans=ans*a%mod;
a=(a*a)%mod;
b>>=1;
}
return ans%mod;
}
ll n,p;
ll c[5000010]={1};
ll f[5000010];
int main()
{
scanf("%lld%lld",&n,&p);
for(int i=1;i<=n;i++)
c[i]=(c[i-1]*i)%p;
f[n]=mul(c[n],p-2,p); //获得inv(n!)
for(int i=n-1;i>=1;i--) //递推阶乘的逆元
f[i]=(f[i+1]*(i+1))%p;
for(int j=1;j<=n;j++) //转化并输出
printf("%lld\n",(f[j]*c[j-1])%p);
}
7.扩展中国剩余定理(exCRT)
写在前面:exCRT完全可以求解CRT问题,而由于其优秀性质,可以使模数不互质
洛谷P4777
给定
让我们来改变一下格式:
把(1)(2)相减得:
用
由于我们知道,
那么x的全解是
由
即:
则:
把(3)(4)再联立
即可求解
#include <bits/stdc++.h>
#define ll long long
using namespace std;
int n;
const int maxn = 1e5+7;
ll ai[maxn],bi[maxn];
ll exgcd(ll a,ll b,ll &x,ll &y)
{
if(!b)
{
x=1;
y=0;
return a;
}
ll gcd = exgcd(b,a%b,x,y);
ll p = x;
x = y;
y = p - (a/b)*y;
return gcd;
}
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 excrt()
{
ll x,y,k;
ll M = ai[1],ans = bi[1];//第一个方程的解特判,分别是模数,等于数
for(int i=2;i<=n;i++)
{
ll a = M,b = ai[i],c = ((bi[i] - ans)%b +b)%b;//ax ≡c(mod b)
ll gcd = exgcd(a,b,x,y),bg = b / gcd;
if(c % gcd != 0) return -1;
x =mul(x , c / gcd , bg);
ans += x * M;//更新前k个方程组的答案
M *= bg;//M为前k个模数的lcm
ans = (ans % M + M) % M;
}
return (ans % M + M) % M;
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%lld%lld",&ai[i],&bi[i]);//模数,乘数
}
printf("%lld",excrt());
return 0;
}
看过代码之后,我们再考虑具体的编程实现问题。
假设已经求出前
且有
(代码实现中用的就是
则前
那么对于加入第
我们就是要求一个正整数t,使得
转化一下上述式子得
对于这个式子我们已经可以通过扩展欧几里得求解t
若该同余式无解,则整个方程组无解, 若有,则前k个同余式组成的方程组的一个解解为
所以整个算法的思路就是求解k次扩展欧几里得
8.关于取模问题
仍要记得开
取模铭记“能取就取”!
基础公式:
一定要记住乘法仍有可能爆
快速幂+取模
可以用分治的方法求快速幂,并且边运行边取模
ll f(ll x,ll y)
{
ll ans=1;
while(y)
{
if(y&1) ans=ans*x%k;
x=x*x%k;
y>>=1;
}
return ans%k;
}
(快速)龟速乘
主要是为了取模
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;
}
9.欧拉函数
(1) 基本介绍
首先我们可以通过容斥原理,我们思考:
给出n唯一分级式
那么我们应该先从总数n中减去
即为
再加上“同时是两个素因子的倍数的个数”
再减去“同时是三个素因子的倍数的个数”
一个比较厉害的公式就是
这个这一项
下一步有一个不显然的结论:
可以上面公式可以变形为:
或通式:
这样只需要
为什么这个式子等价?
展开式子的每一项是从每个括号各选一个(1或
举例:
(2) 介绍一些定理
定理1:对于
定理2:
定理3:
定理4:欧拉函数是积性函数,当
定理5:
定理6:
定理7:
(3) 求欧拉函数
若没给出唯一分解式,可以通过和质因数分解试除法相似的方法求出。
int euler(int n)
{
int ret=n;
for(int i=2;i*i<=n;i++)
{
if(n%i==0)
{
ret-=ret/i;
while(n%i==0)
{
n/=i;
}
}
}
if(n>1)
ret-=ret/n;
return ret;
}
对于此函数的详细解释可以按照定理3:
由于唯一分解定理,我们一定可以吧一个整数分解为很多素数乘积的形式。
那么对于其中一个素数
为什么这样?
我们可以吧这个
由于都是唯一分解定理后,都是素数的乘积,那么可以根据定理4证明所有情况。
为什么n>1直接除?
因为对于质因数分解的形式,
(4) 筛法求欧拉函数
//筛选法打欧拉函数表
#define Max 1000001
int euler[Max];
void Init(){
euler[1]=1;
for(int i=2;i<Max;i++)
euler[i]=i;
for(int i=2;i<Max;i++)
if(euler[i]==i)
for(int j=i;j<Max;j+=i)
euler[j]=euler[j]/i*(i-1);//先进行除法是为了防止中间数据的溢出
}
10.欧拉定理和费马小定理
欧拉定理
对于任何两个互质的正整数(
有
所以
费马小定理
欧拉定理中
应用:利用欧拉函数求不超过n且与n互素的正整数的个数,其次可以利用欧拉定理与费马小定理来求得一个逆元,欧拉定理中的m适用任何正整数,而费马小定理只能要求m是质数。
拓展欧拉定理
扩展欧拉定理无需
当