质数和约数

目录

前言

质数

试除法求质数

第一次优化

第二次优化

分解质因数

试除法求质因数

筛质数

朴素筛法

埃式筛法

线性筛法

约数

试除法求约数

约数个数

约数之和

最大公约数


前言

今天要讲解的是数论部分,包括求质数,筛质数,分解质因数,求约数,约数个数,约数之和,求最大公因数。

质数

我们将在所有正整数 (非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; //筛掉
        } //质数部分
    }
}

可以发现只是用质数 筛掉所有数字,由算数基本定理 ,我们通过每个数的质因数去筛掉这个数字

那么时间复杂度 为什么是接近线性呢?

补充一个定理,从1n中的质数数量 大约为**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)**,

我们设**dab的公约数** ,则**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)**

相关推荐
Joyee69120 分钟前
文本领域的在线协作引擎——OT 算法的原理与应用
算法
周Echo周24 分钟前
5、vim编辑和shell编程【超详细】
java·linux·c++·后端·编辑器·vim
lisw0526 分钟前
排序算法可视化工具——基于React的交互式应用
算法·react.js·排序算法
榆榆欸26 分钟前
6.实现 Reactor 模式的 EventLoop 和 Server 类
linux·服务器·网络·c++·tcp/ip
奋进的小暄44 分钟前
贪心算法(13)(java)合并区间
算法
快来卷java1 小时前
深入剖析雪花算法:分布式ID生成的核心方案
java·数据库·redis·分布式·算法·缓存·dreamweaver
阿巴~阿巴~1 小时前
C/C++蓝桥杯算法真题打卡(Day11)
算法
tpoog1 小时前
[MySQL]数据类型
android·开发语言·数据库·mysql·算法·adb·贪心算法
刚入门的大一新生1 小时前
排序算法3-交换排序
算法·排序算法
虾球xz2 小时前
游戏引擎学习第193天
c++·学习·游戏引擎