【笔记】基本数论
一、最大公约数,欧几里得算法,GCD
这一部分非常简单,普及-难度的算法。
首先是一段初学者都会的递归求 GCD 的辗转相除法:
int gcd(int a,int b){
if(b==0) return a;
else return gcd(b,a%b);
}
这也被称为欧几里得算法。
这里重点探讨一下时间复杂度。
Case 1: \(b>a\)
一次递归后 \(a>b\)。
Case 2: \(a>b\)
当 \(b>\frac{a}{2}\) 时,有 \(a\bmod b=a-b<\frac{a}{2}\);
当 \(b<\frac{a}{2}\) 时,有 \(a\bmod b<b<\frac{a}{2}\)。
经过两次递归后 \(a\) 小于原来的一半。
因此时间复杂度为 \(O(\log(\max(a,b)))\)。
看几道例题。
Luogu P1029 [NOIP2001 普及组] 最大公约数和最小公倍数问题
普通暴力最多 \(80\)pts,一些优化暴力能 AC,不过我们来看数学正解。
不妨设 \(P=x \times a,Q=y \times b\),考虑 \(a,b\) 的性质。
首先易得 \(a,b\) 需要互质。假设 \(a,b\) 有共同的质因子 \(k\),那么 \(\gcd(P,Q)=x \times k\),不符合题意。
于是考虑 \(a,b\) 在互质情况下正整数解的个数。
首先考虑 \(a,b\) 互质的条件。相同的质因子一定只出现在一个数里。
然后考虑 \(a,b\) 该如何求出。我们将题目给的和我们设的等式一起列出:
移项得:
于是我们求出 \(\frac{y}{x}\),并将其分解质因数,运用乘法原理,一旦有新的质因数那么就答案加一并乘二。
记得特判 \(y\) 与 \(x\) 除不开的情况。
时间复杂度为质因数分解的 \(O(\sqrt n)\)。
代码:
#include<iostream>
#include<cmath>
#define int long long
using namespace std;
const int maxn=1e5+10;
int x,y;
double d;
int a,b,yin[maxn],tot,ans;
int gcd(int a,int b){
if(b==0) return a;
else return gcd(b,a%b);
}
signed main(){
cin>>x>>y;
d=1.0*y/x;
a=floor(d);
b=ceil(d);
if(a!=b){
cout<<0;
return 0;
}
if(a==1){
cout<<1;
return 0;
}
ans=1;
for(int i=2;i*i<=a;i++){
if(!(a%i)&&gcd(i,a/i)==1) ans++;
}
cout<<ans*2;
return 0;
}
Luogu P1372 又是毕业季I
暴力是不现实的,\(k,n \le 10^9\)。
这时候我们来考虑公约数的最大是否有一个固定的规律。
我们知道两个有倍数关系的数,最大公约数是其中较小的那一个,这时候最大公约数能取到最大。
而为了使两个数中较小的那一个取到最大,那么这个数就要尽可能接近最大值。于是我们便从后往前取 \(k\) 个有倍数关系的数,不难得出其中最小的数是 \(\frac{n}{k}\),输出这个数即可。
时间复杂度 \(O(1)\)。
代码不给了。
这里附上 kkksc03 站长的证明过程。
设可能的最大公约数为 \(a\),取出的 \(k\) 个数为 \(x_1,x_2,...,x_k\),且满足 \(x_1<x_2<...<x_k\),那么有 \(x_1 \ge a,x_2 \ge 2a,...,x_k \ge ka\)。又因为 \(x_k \le n\),所以 \(n \ge ka\),即 \(a \le \frac{n}{k}\),又因为 \(a \in Z_+\),所以 \(a \le \lfloor \frac{n}{k} \rfloor\)。
P8682 [蓝桥杯 2019 省 B] 等差数列
题目给出了首项和末项,要求最少的项数,即最大的公差。
我们知道,被挖掉一些项的等差数列相邻两项的差一定是真正公差的倍数,所以我们要找到一个尽可能大又小于等于所有相邻两项差的数作为公差。我们发现这就是一个最大公约数问题。于是我们给每相邻两项作差并求最大公约数即可求出最大公差。然后套等差数列项数计算公式即可。
记得先把题目给的序列 sort 一下。
代码:
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn=1e5+10;
int n;
int a[maxn];
int minn=2147483647;
int gcd(int a,int b){
if(b==0) return a;
else return gcd(b,a%b);
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
}
sort(a+1,a+1+n);
minn=a[2]-a[1];
if(a[1]==a[n]){
cout<<n;
return 0;
}
for(int i=3;i<=n;i++){
minn=gcd(minn,a[i]-a[i-1]);
}
cout<<(a[n]-a[1])/minn+1;
return 0;
}
Luogu P2118 [NOIP2014 普及组] 比例简化
此题枚举互质的数,需要用到最大公约数,这里不再展开。
二、扩展欧几里得算法,exGCD
开始上强度。
首先是裴蜀定理,
对于任意整数 \(a,b\),必然有:
步入正题:扩展欧几里得算法。
该算法解决的问题:解不定方程 \(ax+by=c\)。
首先,\(c\) 必须是 \(\gcd(a,b)\) 的倍数,否则无整数解。证明如下:
因为
所以
故 \(c=k \times \gcd(a,b)\)。
接下来,因为 \(\gcd(a,b) \mid c\),所以只考虑 \(ax+by=\gcd(a,b)\),其他方程的解对应乘上等比例的 \(k\) 即可。根据基本欧几里得算法,有
恒等变形,得
我们要求 \(x,y\),就需要先求 \(x{'},y{'}\),从后往前推,用递归实现。我们仿照基本欧几里得,在递归后加上一个回溯求解的操作。但是当 \(b=0\) 时,要把 \(x\) 赋为 \(1\),\(y\) 赋为 \(0\) 进行递归出口。为什么要这样进行出口?因为 \(b=0\),所以显然存在 \(a \times 1+0 \times 0=\gcd(a,0)\),不定方程成立,因此这样出口。回溯求解时套上文公式即可。
需要注意的是,在扩展欧几里得算法中也包含了基本欧几里得,也就是说可以同时求得 \(x,y\) 与 \(\gcd(a,b)\)。
代码:
int a,b,x,y;
int exgcd(int a,int b,int &x,int &y){
if(!b){
x=1,y=0;
return a;
}
int d=exgcd(b,a%b,x,y);
int t=y;
y=x-a/b*y;
x=t;
return d;//返回值为 gcd(a,b),而x和y的值直接在原变量上修改。
}
注意此算法求解的是 \(ax+by=\gcd(a,b)\),如果原方程是 \(ax+by=k\times\gcd(a,b)\),那么 \(x,y\) 也要等比例扩大 \(k\)。
下面来看一些其他应用。
已知 \(ax{'}+by{'}=\gcd(a,b)\) 的整数解 \(x{'}\) 与 \(y{'}\)。
令 \(c=r\times\gcd(a,b)\),则有 \(r=\frac{c}{\gcd(a,b)}\)。
又
则有
于是我们如果知道了这个不定方程的一组整数解,那么其他无穷多组整数解都能被直接求出。
那么如果我们要求 \(x\) 非负且最小,该如何计算呢?
令 \(g=\gcd(a,b)\),\(\operatorname{lcm}(a,b)=a \times b \div g\)。
那么
展开并合并同类项,得
由此可得,\(x\pm k\times\frac{b}{\gcd(a,b)}\) 后均有对应的 \(y\) 解。
那么令 \(t=\frac{b}{\gcd(a,b)}\),\((x\bmod t+t)\bmod t\) 就是 \(x\) 的最小非负整数解。
看几道例题。
Luogu P1516 青蛙的约会
非常经典的扩展欧几里得。
由题意得,两只青蛙如果要相遇,就需要满足:
则
即
不难发现,这就是一个形如 \(ax+by=c\) 的不定方程,其中 \(n-m=a\),\(l=b\),\(x-y=c\),\(k\) 和 \(z\) 是未知数。题目要求的是 \(k\)。
于是直接套扩欧。
不过有一点需要注意,就是 \(\gcd\) 只对非负整数有意义。因此,当 \(a<0\) 时,\(a\) 与 \(c\) 都要取相反数。而 \(b\) 不能取负,因为 \(b\) 本来即正,而 \(t=\frac{b}{\gcd(a,b)}\),如果 \(b<0\) 那么 \(t<0\),\((x\bmod t+t)\bmod t\) 也是负数,就无法找到最小正整数解。
代码:
#include<iostream>
#include<cstdio>
#define int long long
using namespace std;
int x,y,m,n,l,t,k,ans;
int exgcd(int a,int b,int &t,int &k){
if(!b){
t=1,k=0;
return a;
}
int d=exgcd(b,a%b,t,k);
int z=k;
k=t-a/b*k;
t=z;
return d;
}
signed main(){
scanf("%lld%lld%lld%lld%lld",&x,&y,&m,&n,&l);
int a,b,c;
a=n-m;b=l;c=x-y;
if(a<0){
a=-a;
c=-c;
}
ans=exgcd(a,b,t,k);
if(c%ans) cout<<"Impossible";//如果c不是gcd(a,b)的整数倍
else{
t=t*c/ans;
t=(t%(b/ans)+(b/ans))%(b/ans);//求最小正整数解
cout<<t;
}
return 0;
}
Luogu P1082 [NOIP2012 提高组] 同余方程
比上个题还水。
像上个题一样,把同余方程进行变换。
原方程:
即
移项,得
那么我们把 \(-k\) 看成 \(y\),即可得出不定方程
数据保证一定有解,套扩欧,结束。
当然,\(1\) 是任何整数的因数,所以实际上题目不说保证一定有解它也一定有解。
代码:
#include<iostream>
#include<cstdio>
using namespace std;
int a,b,x,y,d;
int exgcd(int a,int b,int &x,int &y){
if(!b){
x=1,y=0;
return a;
}
int d=exgcd(b,a%b,x,y);
int t=y;
y=x-a/b*y;
x=t;
return d;
}
int main(){
cin>>a>>b;
d=exgcd(a,b,x,y);
cout<<(x%b+b)%b;
return 0;
}
三、质数筛,线性筛
同样是普及-的算法。常用的质数筛法有暴力筛、埃氏筛和欧拉筛(也称线性筛)。这里重点讲一下埃氏筛和欧拉筛。
埃拉托色尼筛法(埃氏筛)
这种筛法相对来说比较好理解。核心思想是遍历每个质数,然后把这个质数的倍数全部筛掉。
for(int i=2;i<=n;i++){
if(vis[i]==0){
prim[++tot]=i;
for(int j=2;j<=n/i;j++){
vis[i*j]=1;
}
}
}
可以进一步优化。我们发现如 \(10=2\times 5\)、\(35=5\times 7\) 这样的数会被筛两次,这是坏的。于是我们让 \(5\) 直接从 \(5\times5\) 开始筛,让 \(7\) 直接从 \(7\times7\) 开始筛,就能避免重复筛的情况。
for(int i=2;i<=n;i++){
if(vis[i]==0){
prim[++tot]=i;
for(int j=i;j<=n;j+=i){
vis[j]=1;
}
}
}
然后我们来证明一下时间复杂度。每一次遍历一个质数 \(p\),我们就要标记 \(p\) 的所有倍数,可以近似看为 \(\frac{n}{p}\)。总操作可以近似为 \(\frac{n}{2}+\frac{n}{3}+\frac{n}{5}+...+\frac{n}{p}\),是一个比 \(n\) 倍调和级数要小的数,上限大概在 \(O(\log(\log n))\),因此总时间复杂度为 \(O(n\log(\log n))\)。
埃氏筛交在线性筛模板里稍微卡卡常是能过的。
欧拉筛法(线性筛)
在埃氏筛中,每一个合数都被筛了不止一次,非常浪费。而欧拉筛能够做到每个合数只被其最小的质因子筛掉,时间复杂度 \(O(n)\)。
外层循环枚举 \(2\rightarrow n\),如果其没有被筛掉,加入进质数数组 \(p\) 中。内层循环枚举质数数组 \(p\),将 \(p_j\) 的 \(i\) 倍筛掉。维护一个数组 \(vis\),\(vis_i\) 记录 \(i\) 的最小质因子。如果 \(p_j>vis_i\),那么 \(p_j\) 不是 \(i\) 的最小质因子,自然也就不是 \(i\times p_j\) 的最小质因子,于是直接跳出循环,因为 \((i+x) \times p_j\) 的最小质因子也不可能是 \(p_j\) 了。否则将 \(i\times p_j\) 的最小质因子设为 \(p_j\),继续枚举。
不过要注意枚举 \(j\) 的时候不要越界了。
代码:
for(int i=2;i<=n;i++){
if(!vis[i]){
prim[++tot]=i;
vis[i]=i;
}
for(int j=1;j<=tot&&prim[j]<=n/i;j++){
if(prim[j]>vis[i]) break;
vis[i*prim[j]]=prim[j];
}
}
比对一下埃氏筛和欧拉筛在 Luogu P3383 【模板】线性筛素数 中的提交用时。
四、欧拉函数
定义:小于等于 \(n\) 且与 \(n\) 互质的数的个数。\(n\) 为正整数。
\(φ(1)=1\)。
\(n\) 为质数时,\(φ(n)=n-1\)。
欧拉函数是积性函数。
如果 \(\gcd(a,b)=1\),那么 \(φ(a)\times φ(b)=φ(a \times b)\)。
特别地,当 \(n\) 是奇数时,\(φ(2n)=e(n)\)。
设 \(n\) 的质因子为 \(p_1\) 和 \(p_2\)。
从 \(1 \sim n\) 中去掉 \(p_1,p_2\) 的所有倍数,即为
因为 \(p_1 \times p_2\) 减了两次,所以再加上
结果为
由此可得出通项公式
性质
\(p\) 为质数时,\(φ(p)=p-1\)。因为质数 \(p\) 只有两个正因数,所以除了 \(p\) 之外小于 \(p\) 的正整数都与 \(p\) 互质。
如果 \(\gcd(a,b)=1\),那么 \(φ(a)\times φ(b)=φ(a \times b)\)。这意味着欧拉函数在乘法下具有分解性。
特别地,当 \(n\) 是奇数时,\(φ(2n)=e(n)\)。
如果 \(p\) 和 \(q\) 是两个不同的质数,那么 \(φ(p\times q)=φ(p)\times φ(q)=(p-1)\times(q-1)\)
设 \(p\) 为质数,若 \(p|n\) 且 \(p^2|n\),则 \(φ(n)=φ(\frac{n}{p}) \times p\)。
证明:
展开,待补完。
设 \(p\) 为质数,若 \(p|n\) 但 \(p^2\) 不整除 \(n\),则 \(φ(n)=φ(\frac{n}{p})\times (p-1)\)
证明:
n-1,待补完

浙公网安备 33010602011771号