基础数论笔记
学完基础数论的大部分内容,过来写一个总结,巩固记忆,另外再重新推倒一些公式。
本文章主要涉及的模块:
- 素数
- 最大公因数及最小公倍数
- 快速幂及扩展欧拉定理
- 扩展欧几里得与同余
- 逆元
- 中国剩余定理
素数
定义
因数只有
注意:
两个关于素数的定理
唯一分解定理
对于任意大于
比如
费马小定理
若存在正整数
证明就不给了,有兴趣可以上网找一下。费马小定理在后续会有很多用处。
素数的判别方法
试除法
试判断
由素数的定义可知,素数的因数只有两个。所以我们可以直接暴力枚举除
- 如果有
1<i<x 且i \mid x 就说明x 有超过两个数的因数,所以x 不是素数。 - 如果枚举完了,但还是找不到整除
x 的数,就说明x 是素数。
时间复杂度为
由因数的性质可知,若正整数
根据这一个性质,我们可以只枚举
代码
inline bool is_prime(int x) {
for(register int i = 2;i <= sqrt(x);i ++)
if(x % i == 0)
return false;
return true;
}
但当题目要求求出一串数中的素数时,试除法的时间复杂度就变成了
埃氏筛
素数的第一种筛法。那何为筛法呢?顾名思义,就是把不是素数的数筛掉,这样剩下的就只有素数了。那怎么筛呢?
由唯一分解定理可知,任意一个大于
给大家模拟一下,加深理解:
此时,若我们要筛出
- 首先,我们要特判
1 :
- 接着,我们从
2 开始一个一个的往后筛:
- 此时,我们发现
4 已经被筛掉了,所以我们直接跳过它去筛5 。
- 我们发现,在筛到
7 时,它的倍数已经超出了需要求的范围,所以我们便可以停止了。
重新回看表格,所有的
代码
int n;
bool flag[MAXN];
void prime(int n) {
flag[1] = true;
for(register int i = 2;i <= n;i ++)
if(!flag[i])
for(register int j = 2;j * i <= n;j ++)
flag[j * i] = true;
for(register int i = 1;i <= n;i ++)
if(!flag[i])
cout << i << " ";
return;
}
埃氏筛的时间复杂度是:
线性筛
埃氏筛的时间复杂度已经很优了,但当题目的数据范围达到
顾名思义,线性筛的时间复杂度在埃氏筛上的基础上优化到了
不难发现,许多正整数都有着超过
拿刚刚的例子讲解一下:
在小于
而在线性筛中,
具体的实现方法便是只用小于等于自己最小的素因数的素数来筛掉合数。
算法流程:
-
首先与埃氏筛一样,要特判
1 的情况。 -
然后,把找到的素数都放进一个 prime 数组里面。假设我们现筛到了
i ,则我们便会用 prime 数组中的素数与i 相乘,把相乘所得到的结果都标记为合数。当i 除以 prime 中的素数余数为0 时停止(先筛完再停止)。这样便做到了用最小的素数筛掉合数。
为什么这是正确的呢?我们来证明一下:
设 prime 数组中的素数为
-
若
i 为素数。当
k_i\nmid i 时说明k_i 比i 小,所以k_i 是正整数k_i \times i 最小的素因数。当
k_i \mid i 时说明k_i=i ,此时,k_i 与i 同为k_i \times i 的最小素因数。 -
若
i 不为素数,设其分解素因数形式为:p_1^{a_1}\times p_2^{a_2} \times \cdots \times p_n^{a_n} 。则当
k_i \nmid i 时,同样的,k_i 必然比p_1 要小,所以k_i 肯定是正整数k_i \times i 的最小素因数。当
k_i \mid i 且是第一次整除时说明k_i=p_1 ,此时,k_i 与p_1 同为k_i \times i 的最小素因数。
代码
int prime[MAXN] , cnt;
bool is_prime[MAXN];
inline void Init(int x) {
is_prime[1] = true;
for(register int i = 2;i <= x;i ++) {
if(!is_prime[i])
prime[++ cnt] = i;
for(register int j = 1;j <= cnt && i * j <= x;j ++) {
is_prime[i * prime[j]] = true;
if(i % prime[j])
break;
}
}
return;
}
欧拉函数
在做关于素数的题目时经常需要用到关于欧拉函数的知识,并且求法相似,所以把它们俩放在一起讲。
求单个数的欧拉函数
定义
对于任意正整数
举几个例子:
怎样求出一个数的欧拉函数呢?公式呈上:
对于任意正整数
化简得:
有这个公式,我们可以求出单个正整数的欧拉函数。即在分解素因数时计算欧拉函数。
时间复杂度:
代码
注意:在素因数分解完后如果
inline int phi(int n) {
int ans = n;
for(register int i = 2;i <= sqrt(n);i ++)
if(n % i == 0) {
ans = ans / i * (i - 1);
while (n % i == 0)
n /= i;
}
if(n > 1)
ans = ans / n * (n - 1);
return ans;
}
欧拉函数公式的推导过程
接下来,我们一起推导在
首先要知道几个性质:
- 对于任意素数
x ,有\varphi(x)=x-1 。这个很好理解,即x 与所有小于它的数互素。 -
对于任意素数
p ,有\varphi(p^k)= p^k-p^{k-1} 。为什么呢?证明:因为
p 是素数,所以p^k 的素因数只含有p 。那么在小于等于p^k 的数中与其不互素的数便是p ,2\times p ,3\times p ,\cdots ,p^{k-1}\times p ,共p^{k-1} 个。得证。 - 欧拉函数是积性函数,但不是完全积性函数。这意味着
\varphi(m\times n)=\varphi(m)\times \varphi(n) 成立的前提是\gcd(m,n)=1 。
了解完这几个性质后,我们开始推导:
设
因为
所以由性质 3 得:
再由性质 2 得:
提取公因数得:
最后把前
推导完毕。
欧拉函数的线性筛法
和素数一样,这样的算法求单个数的欧拉函数时间复杂度很优,但当题目需要求一串数的欧拉函数时便处理不了了。此时的时间复杂度便是
那求一串数欧拉函数是否和求素数一样,有跟快甚至线性的方法呢?当然有,由刚刚的几个性质,我们可以在线性筛的基础上改进,在
这里我们需要再引入一个性质:
-
若
i\bmod p=0 ,则有:\varphi(p\times i)=\varphi(i)\times p 。证明:
设
b=\gcd(n , i) ;因为
n ,i 不互素,所以n=k_1b ,i=k_2b 。因为
n+i=(k_1+k_2)b ,所以n+i 与i 不互素。因为 $n+i$ 与 $i$ 不互素,所以 $[1+i,i+i]$,即 $(i,2i]$ 中与 $i$ 不互素的整数也是 $i-\varphi(i)$ 个,所以 $[1,i\times p]$ 中与 $i$ 不互素的整数有 $p\times i-p\times \varphi(i)$ 个。 因为 $i\bmod p = 0$,$i$ 与 $p$ 没有不同的因数,$[1,i\times p]$ 中与 $i\times p$ 不互素的整数数量 $p\times i-p\times \varphi(i)=i\times p-\varphi(i\times p)$。 所以:$\varphi(i\times p)=p\times \varphi(i)$。
我们一起边回顾线性筛代码,边了解求欧拉函数的方法:
定义phi[MAXN]存储欧拉函数。
首先,在开始筛之前我们先需要特判
is_prime[1] = true;
phi[1] = 1;
然后我们开始找素数。在找的过程中,如果遇到了素数
for(register int i = 2;i <= n;i ++) {
if(!is_prime[i]) {
prime[++ cnt] = i;
phi[i] = i - 1;
}
...
}
最后,在用找到的数
若用来与
若用来与
for(register int j = 1; j <= cnt , i * prime[j] <= n; j ++) {
is_prime[i * prime[j]] = true;
if(i % prime[j] == 0) {
phi[i * prime[j]] = phi[i] * prime[j];
break;
}
phi[i * prime[j]] = phi[i] * phi[prime[j]];
}
完整代码
int phi[MAXN] , prime[MAXN] , cnt;
bool is_prime[MAXN];
inline void Init_phi(int n) {
is_prime[1] = true;
phi[1] = 1;
for(register int i = 2;i <= n;i ++) {
if(!is_prime[i]) {
prime[++ cnt] = i;
phi[i] = i - 1;
}
for(register int j = 1;j <= cnt , i * prime[j] <= n;j ++) {
is_prime[i * prime[j]] = true;
if(i % prime[j] == 0) {
phi[i * prime[j]] = phi[i] * prime[j];
break;
}
phi[i * prime[j]] = phi[i] * phi[prime[j]];
}
}
return;
}
最大公因数与最小公倍数
这一章内容比较少,主要讲的是最大公因数的几种求法。
最大公因数
定义
在两个数的公因数中最大的那个叫做这两个数的最大公因数。
我们一般用
接下来,我给大家介绍
穷举法
顾名思义,即暴力枚举:从两个数中较小的一个开始从大到小枚举,遇到的第一个可以同时整除这两个数的数就是它们的最大公因数。
至于为什么要从较小的一个开始枚举,因为两个数的公因数只可能小于等于较小的那个数。
时间复杂度:
代码
inline int gcd(int a , int b) {
int x = min(a , b);
for(register int i = x;i >= 1;i --)
if(a % i == 0 && b % i == 0)
return i;
}
辗转相除法
定义
辗转相除法,又称欧几里得算法,是一种用来求最大公因数的算法。其算法流程主要基于这么一个式子:
由这个式子,我们可以想到一种递归求最大公因数的写法:
代码
int gcd(int a , int b) {
if(!b)
return a;
return gcd(b , a % b);
}
当然,也可以把这个过程改写成一个循环:
inline int gcd(int a , int b) {
int r = a % b;
while(r) {
a = b;
b = r;
r = a % b;
}
return b;
}
时间复杂度均为:
辗转相除法的证明
下面给出证明:
设
则必有:
移项得:
两边同时除以
因为
而
所以,
现在我们证出
更相减损法
定义
更相减损法,即把两数不断相减,直到两数相等。其实主要思想和辗转相除法类似。
具体算法流程是:
试求
-
若
2\mid a 且2\mid b ,则先将两数所含的2 给除掉。 -
接着,我们不断用大数减小数,直到两数相等。此时相等的两数乘上约掉的
2 即为原本两数的最大公因数。
用公式表达就是:
我们发现,其实第一步可以省略,直接执行第二步即可。
时间复杂度与辗转相除法一样:
代码
代码实现比较简单:
inline int gcd(int a , int b) {
while(a != b) {
if(a > b)
a = a - b;
else
b = b - a;
}
return a;
}
更相减损法的证明
第一个公式十分好证:
因为
接下来我们来证明第二个公式:
设
则
令
由此可得:
二进制算法
对于处理数位较少的数的最大公因数,使用辗转相除法就可以了,但当数位达到上百位时便需要用到高精度。此时,辗转相除法就不那么适用了。对于这类问题,我们可以用支持高精度的二进制算法。
算法流程如下:
若
否则,分情况讨论:
-
若
a 和b 均为偶数,则:\gcd(a,b)=2\times\gcd(a\div 2,b\div 2) 。 -
若
a 为偶数,b 为奇数,则\gcd(a,b)=\gcd(a\div 2,b) 。 -
若
a 与b 均为奇数,则\gcd(a,b)=\gcd(a-b,b) 。
最终答案即是第一个操作中被约掉的
其实二进制算法和更相减损法十分相似。这里就不给证明了。
代码
这里给出一个完整的高精度最大公因数的代码:
#include<iostream>
#include<iomanip>
#include<cstring>
#include<cmath>
#include<stack>
#include<queue>
#define int long long
using namespace std;
string s1 , s2;
int a[3005] , b[3005] , c[3005];
int len1 , len2;
inline void div1(int a[]) {
int r = 0;
for(register int i = len1;i >= 1;i --) {
a[i] = a[i] + r * 10;
r = a[i] % 2;
a[i] /= 2;
}
while(!a[len1] && len1 > 1)
len1 --;
return;
}
inline void div2(int b[]) {
int r = 0;
for(register int i = len2;i >= 1;i --) {
b[i] = b[i] + r * 10;
r = b[i] % 2;
b[i] /= 2;
}
while(!b[len2] && len2 > 1)
len2 --;
return;
}
inline bool equal(int a[] , int b[]) {
if(len1 != len2)
return false;
for(register int i = 1;i <= len1;i ++)
if(a[i] != b[i])
return false;
return true;
}
inline void poww(int a[] , int x) {
for(register int i = 1;i <= 3000;i ++)
a[i] *= x;
for(register int i = 1;i <= 3000;i ++) {
a[i + 1] += a[i] / 10;
a[i] %= 10;
}
len1 = 3000;
while(!a[len1] && len1 > 1)
len1 --;
for(register int i = len1;i >= 1;i --)
cout << a[i];
return;
}
inline bool bigger(int a[] , int b[]) {
if(len2 < len1)
return false;
if(len2 > len1)
return true;
for(register int i = len1;i >= 1;i --)
if(b[i] > a[i])
return true;
else if(a[i] > b[i])
return false;
return false;
}
inline void change(int a[] , int b[]) {
for(register int i = 1;i <= len1;i ++)
c[i] = a[i];
for(register int i = 1;i <= len2;i ++)
a[i] = b[i];
for(register int i = 1;i <= len1;i ++)
b[i] = c[i];
int k = len1;
len1 = len2;
len2 = k;
for(register int i = 3000;i > len2;i --)
b[i] = 0;
for(register int i = 3000;i > len1;i --)
a[i] = 0;
return;
}
inline void mi(int a[] , int b[]) {
for(register int i = 1;i <= len1;i ++)
a[i] = a[i] - b[i];
for(register int i = 1;i <= len1;i ++)
if(a[i] < 0) {
a[i] += 10;
a[i + 1] --;
}
while(!a[len1] && len1 > 1)
len1 --;
return;
}
inline int ksm(int x) {
int result = 1 , base = 2;
while(x > 0) {
if(x & 1)
result = result * base;
x >>= 1;
base = (base * base);
}
return result;
}
//精髓部分
inline void gcd(int a[] , int b[]) {
int i , j;
for(i = 0;a[1] & 0;i ++)
div1(a);
for(j = 0;b[1] & 0;j ++)
div2(b);
if(i > j)
i = j;
int k = ksm(i);
while(true) {
if(equal(a , b)) {
poww(a , k);
exit(0);
}
if(bigger(a , b))
change(a , b);
mi(a , b);
while(a[1] & 0)
div1(a);
}
}
signed main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> s1 >> s2;
len1 = s1.size();
len2 = s2.size();
for(register int i = 0;i < len1;i ++)
a[len1 - i] = s1[i] - '0';
for(register int i = 0;i < len2;i ++)
b[len2 - i] = s2[i] - '0';
gcd(a , b);
return 0;
}
c++ 自带函数
在这么多求最大公因数的算法中,应用的比较的多的就只有辗转相除法。但其实 c++ 内部也有自带的求最大公因数的函数,即:__gcd() 函数,用法和手写的一样。若要求 cout << __gcd(a,b)。
最小公倍数
定义
两数的公倍数中最小的那个数被称作这两个数的最小公倍数。
我们一般用
最小公倍数的求法
易得:
这个公式十分容易得到,这里就不给推导过程了。
个人建议先除以
代码
int gcd(int a , int b) {
if(!b)
return a;
return gcd(b , a % b);
}
inline int lcm(int a , int b) {
return a / gcd(a , b) * b;
}
快速幂与扩展欧拉定理
把这两个知识点放一起是因为扩展欧拉定理需要用到快速幂。
快速幂
定义
快速幂,即很快的做幂运算,并且在求幂的同时支持取模操作。但这个算法究竟是怎么实现的,我们来探究一下。
快速幂的原理
假设我们需要求
最暴力的方法就是一个一个乘:
这样的时间复杂度是:
当然,聪明的你们(不是我)一定发现了一个简便的方法,那就是让底数不断平方,然后指数不断除以
不难发现,这样求幂只运算了
求
我们还是先按之前的方法做:
现在我们发现指数变成了奇数,不能再除以
经过处理,我们就可以继续重复原来的步骤了:
这两个步骤就是快速幂算法的主要流程。
具体实现:
我们可以定义变量 result,记得一定要初始化为 result乘上底数,否则指数除以二,底数平方。
代码实现
由上面的两个步骤,我们可以写出基本的算法框架:
int ksm(int base , int x) {
int result = 1;
while(x) {
if(x % 2 == 1)
result *= base;
x /= 2;
base *= base;
}
return result;
}
但这还不是最快的算法。我们可以将取模运算以及除法运算改成位运算:
inline int ksm(int base , int x) {
int result = 1;
while(x) {
if(x & 1)
result *= base;
x >>= 1;
base *= base;
}
return;
}
欧拉定理与扩展欧拉定理
欧拉定理
定义
对于
其实费马小定理就是欧拉定理的特殊情况,即当
欧拉定理的证明
设
则这些数模
接下来,我们来证明集合
我们先证这些数模
考虑反证法:
若
则有
即
因为
接下来,我们证余数与
因为
所以
所以
把
同时约掉
得证。
欧拉定理的推论与证明
若
下面给出证明:
设
把
得证。
扩展欧拉定理
在大部分情况下,我们都使用扩展欧拉定理,因为它可以处理的情况更多。
定义
对于
-
对于第一种情况,即
b\ge\varphi(m) 时我们可以用公式化简来求解。 -
对于第二种情况,我们可以直接使用快速幂算出结果。
算法流程十分简单,我们只需要判断
代码
inline int phi(int x) {
int ans = x;
for(register int i = 2;i <= sqrt(x);i ++)
if(x % i == 0) {
ans = ans / i * (i - 1);
while(x % i == 0)
x /= i;
}
if(x > 0)
ans = ans / x * (x - 1);
return ans;
}
inline int ksm(int base , int x , int m) {
int result = 1;
while(x) {
if(x & 1)
result = result * base % m;
x >>= 1;
base = base * base % m;
}
return result;
}
inline int solve(int a , int b , int m) {
int phi_m = phi(m);
if(b >= phi_m)
return ksm(a , b % phi_m + phi_m , m);
else
return ksm(a , b , m);
}
证明不放了,以后再 update 吧。
扩展欧几里得与同余
线性同余方程需要用到扩展欧几里得来求解。
同余
简单讲一下同余。
定义
若整数
性质
余数的性质:
- 可加性。
- 可减性。
- 可乘性。
- 可乘方性。
这几个性质光看应该就能理解了,就不过多赘述了。
注意,余数没有可除性!
扩展欧几里得
我们先看一个定理。
裴蜀定理
定义
对于
感性理解一下,不放证明了。
这个定理可以很好的帮助我们判断线性方程是否有整数解,非常有用。
扩展欧几里得算法
扩展欧几里得的特解
对于
这是怎么得到得呢?接下来给出推导过程。
扩展欧几里得的推导过程
同样的,我们可以得到另外一个方程:
把
整理一下等式左边得:
在
将
同时,我们还有:
对比两式得到:
推导完毕。
扩展欧几里得的算法流程
得到了特解,我们该怎么求这个特解呢?
其实这是一个递归的过程。
不难发现,在不断求解的过程中,等式右边的
接着,我们便可以根据上面推导出来的式子不断递归回去而得到特解。
代码
int exgcd(int a , int b , int &x , int &y) {
if(!b) {
x = 1;
y = 0;
return a;
}
int r = exgcd(b , a % b , x , y);
int t = x;
x = y;
y = t - a / b * y;
return r;
}
扩展欧几里得的通解与最小正整数解
在得到了
我们还可以得到该方程的最小正整数解,即:
扩展欧几里得的应用
扩展欧几里得在许多地方都有所应用。
这里先讲解用其求解线性方程。
形如
首先,由裴蜀定理,在
然后,容易想到先求出
设求得的解为
有:
此时,我们可以在等式两边同时乘上
若题目要求最小正整数解,则按上面的公式算一遍即可。
扩展欧几里得还能用来求解一些同余方程。
比如若要求
为了求解,我们可以对这个方程进行一些变换:
原方程其实等价于:
为什么
对上式进一步简化,得到:
此时便可以用扩展欧几里得求解了。
逆元
定义
若整数
本文中讨论的逆元为模意义下的乘法逆元,即
逆元的求法
本文主要介绍逆元的两种求法。
费马小定理求逆元
在上文中提到的费马小定理就可以用来求解特定情况下数的逆元。
费马小定理的式子是:
现在我们对等式右侧进行变化,可得:
将得到的式子与逆元的定义式对比:
可得
由于计算
注意:该算法只能在
代码
此处默认
int a , p;
inline int ksm(int base , int x , int mod) {
int result = 1;
while(x) {
if(x & 1)
result = (result * base) % mod;
x >>= 1;
base = (base * base) % mod;
}
return result;
}
inline int solve(int a , int p) {
return ksm(a , p - 2 , p);
}
扩展欧几里得算法求逆元
不难发现,逆元的定义式其实就是一个线性同余方程,那自然可以用扩展欧几里得来求解。
具体推导过程和讲解扩展欧几里得算法时的类似,就不再写一次了。这里直接给出最终的等式:
得出等式后,直接求解即可。不过该算法求出来的
代码
int a , p;
int exgcd(int a , int b , int &x , int &y) {
if(!b) {
x = 1;
y = 0;
return a;
}
int r = exgcd(b , a % b , x , y);
int t = x;
x = y;
y = t - a / b * y;
return r;
}
inline int solve(int a , int p) {
int x , y;
int r = exgcd(a , p , x , y);
int k = p / r;
x = (x % k + k) % k;
return x;
}
逆元的作用
众所周知,模运算符合加法、减法和乘法的性质,但并不满足除法的性质。这也就意味着:
什么意思呢?即:
其中
这是怎么来的,下面给出推导过程:
推导完毕。
中国剩余定理
就详见这里了。
后记
写了一些,但 BSGS 等还没写,以后在 update。