目录
前言
今天要讲解的是数论部分,包括求质数,筛质数,分解质因数,求约数,约数个数,约数之和,求最大公因数。
质数
我们将在所有正整数 (非1
)中只有**1
和它本身** 能整除该数的数字叫做质数。
试除法求质数
根据质数的性质,可以通过枚举2 ~ n - 1
中的所有数来判断n
是不是质数,代码如下:
cpp
bool is_prime(int n)
{
for(int i = 2; i < n; i++)
if(n % i == 0)
return false;
return true;
}
可以发现时间复杂度是**O(n)
**,已经很慢了,需要优化。
第一次优化
我们知道乘法是一个二元运算符 ,每次进行乘法运算时都有两个数字 ,所以所有能整除n
的数字一定是成对出现的。
就比如12
的所有约数可以为
1, 12
2, 6
3, 4
所以实际上我们只需要枚举到**根号n
**即可,代码为:
cpp
bool is_prime(int n)
{
for(int i = 2; i <= n / i; i++) //注意这里为等号(下取整),推荐使用这种方法确定边界,可以避免溢出。
if(n % i == 0)
return false;
return true;
}
时间复杂度就变成了根号n
,不过主播习惯再优化一次。(一般情况下优化到这里即可)
第二次优化
除2
以外的所有偶数 都不是 质数,所以我们可以手动跳过所有的偶数,这样时间复杂度就变成了sqrt(n)/2
这样优化一下常数也就变得很小了,代码:
cpp
bool is_prime(int n)
{
for(int i = 2; i < n; i += (i & 1) + 1)
if(n % i == 0)
return false;
return true;
}
注意:为节省篇幅 ,主播在之后的代码中都会默认 加上这两条优化,特别的 ,需要证明 的部分会证明可行性。
分解质因数
由算数基本定理(唯一分解定理) ,任何整数 都可以唯一 的表示成
这样的形式(任意两个数字之间不冲突),其中q
均为质数 ,每个q
被称为质因子。
试除法求质因数
思想与试除法求质数相同,依次遍历每个质数判断都否整除,代码:
cpp
int divisor(int n)
{
for(int i = 2; i <= n / i; i += (i & 1) + 1)
{
if(n % i == 0)
{
int s = 0;
while(n % i == 0)
{
n /= i;
s++; //计算
}
}
}
if(n > 1) n;
return l;
}
来讲解为什么可以只枚举到**sqrt(n)
**。
因为任何正整数n
至多 存在一个质因子大于 sqrt(n)
。
为什么?我们可以使用反证法 ,假设存在多个 质因子大于sqrt(n)
,可以发现最后的数字是一定比n
大的,所有假设不成立。
所以对于所有的数字只需要枚举前**sqrt(n)
** 个数,随后特判 一下最后的数字即可。
可能会有小伙伴好奇不是枚举质数吗?为什么我要枚举所有的数判断,有这个疑问的小伙伴先不要着急,我会在下面的筛质数中讲解为什么这样可行。
筛质数
筛质数的原理依旧是基于算数基本定理。
朴素筛法
朴素筛法的思路很简单,根据质数的性质,一个数字的倍数一定不是质数。
代码:
cpp
bool read[N];
void prime(int n)
{
read[1] = read[0] = true;
for(int i = 2; i <= n / i; i++)
{
if(!read[i]){} //质数部分
for(int j = i * i; j <= n; j *= i)
read[j] = true; //筛掉
}
}
可以发现朴素筛法的时间复杂度是**sqrt(n) * logn
** 的,很慢,我们一般情况下不会用这个。
埃式筛法
在朴素筛法上进行一点改变 即可将时间复杂度优化到接近线性,先来看代码。
cpp
bool read[N];
void prime(int n)
{
read[1] = read[0] = true;
for(int i = 2; i <= n / i; i += (i & 1) + 1)
{
if(!read[i])
{
for(int j = i * i; j <= n; j *= i)
read[j] = true; //筛掉
} //质数部分
}
}
可以发现只是用质数 筛掉所有数字,由算数基本定理 ,我们通过每个数的质因数去筛掉这个数字。
那么时间复杂度 为什么是接近线性呢?
补充一个定理,从1
到n
中的质数数量 大约为**n/lnn
** 个,而我们每次用质数筛掉一个数字的循环次数 为**n/x
** ,x
为那个质数。
所以我们总共 要筛**(n/x1 + n/x2 + n/x3 ...)
** 次,我们将n
提出 ,剩下的部分可以看成是一个调和级数 ,所以结果为**n * lnn
** ,而由于大概有**n/lnn
** 个质数,所以我们实际上只是取了调和级数的一部分 ,而这一部分可以看作是均分在调和级数上 的,由某个定理 (哪个定理我忘记了),最终的时间复杂度就为**O(nloglogn)
接近线性**。
当然还可以再加上前面的小优化,因为所有除2
以外的偶数 都不是质数 ,所以对于这些数直接跳过 就好,这样优化下来时间复杂度就更接近线性了。
现在我们再回看分解质因数的代码,其实也可以看作是每次在用质数筛掉其余非质数的因子 ,这就保证了我们每次枚举到的数字一定是质数。
观察埃式筛法 ,我们可以发现即使每次都使用质数去筛掉合数,但是依旧会有很多重合 的部分,每个数字都会被筛掉多次 ,所以需要优化 ,如何优化就要看我们的线性筛法。
线性筛法
埃式筛法本身已经足够优秀 了,但是在数据量在**1e7
** 的情况下会明显吃力 ,所以我们需要线性筛法。
线性筛法的思路与埃式筛法类似 ,依旧是基于算数基本定理 ,只不过不同的是,线性筛法是利用一个数字的最小质因数筛掉 它,这样就保证了每个数字只会被筛一次,先来看代码:
cpp
bool read[N];
void prime(int n)
{
vector<int> vtr; //将质数存起来
for(int i = 2; i <= n / i; i += (i & 1) + 1)
{
if(!read[i])vtr.push_back(i);
for(int j = 0; vtr[j] <= n / i; j++)
{
read[i * vtr[j]] = true; //筛掉
if(i % vtr[j]) break;
}
}
}
边界 和跳过偶数的优化我不再多说,和前面的思路一样。
我们来判断这种情况是否正确,由算数基本定理 ,每个合数是一定存在最小的质因子的 ,所以只需要判断我们的代码精准 的用一个数字的最小质因子去筛掉这个数字即可,先看这段代码。
if(i % vtr[j]) break;
这段代码就保证了前面所有的质数都不是i
的质因子,所以**vtr[j]
一定是i
的最小质因子**,同理对于这行代码
read[i * vtr[j]] = true; //筛掉
因为我们保证了枚举了所有的质数一定小于等于i
的最小质因数 ,所以**vtr[j]
** 就一定 是**i * vtr[j]
** 的最小质因子,所以是正确的。
约数
与质数不同,约数为从1 ~ n
中所有能整除 n
的数字。
试除法求约数
与试除法求质数的思路相同
cpp
int l = 0;
for(int i = 1; i <= n; i++)
if(n % i)
l++;
同样的 ,由乘法的性质 我们依旧可以只枚举到sqrt(n)
cpp
int l = 0;
for(int i = 1; i <= n /i; i++)
{
if(n % i)
{
if(i == n / i) l += 1;
else l += 2;
}
}
注意:这里不能跳过偶数。
约数个数
由算数基本定理一个合数n
可以表示为下面的形式。
由约数的性质 以及排列组合 很容易就可以得到约数个数 为:
代码:
cpp
int divisor_num(int n)
{
int l = 1; //1。
for(int i = 2; i <= n / i; i += (i & 1) + 1)
{
if(n % i == 0)
{
int s = 0;
while(n % i == 0)
{
n /= i;
s++; //计算
}
l *= s + 1; //组合数
}
}
if(n > 1) l *= 2;
return l;
}
约数之和
依旧是算数基本定理
由多项式定理 可得所有的方案的总和为
对于这部分不理解的小伙伴可以先回忆一下二项式定理 ,随后再推广至多项式。
最大公约数
GCD
这个函数相信大家都不陌生,就是求最大公约数,先来说代码。
cpp
int gcd(int a, int b)
{
if(b == 0)return a;
return gcd(b, a % b);
}
要证明代码的正确性就是要证明**gcd(a, b) == gcd(b, a % b)
**,
我们设**d
是a
和b
的公约数** ,则**d
也是a + b
的约数** ,同理**d
也是ax + by
的约数**。
我们设
a = k * b + r
移项得:
r = a - k * b
根据上面得性质可以得到**d
也是a - k * b的
约数** ,所以**gcd(a, b) == gcd(b, a % b)
**