数学知识-质数

质数

1. 试除法判定质数

1.1 算法描述

试除法判定质数的思路比较简单,即对于给定的数字num,遍历从2到num的所有数字,若存在i使得num%i==0,那么num就不是质数。

但事实上,合数的因子是成对出现的,例如12的约数3,4,所以我们没有必要遍历到num,遍历到sqrt(num)即可。

1.2 代码实现

需要注意的是,在对num遍历的时候有以下几种写法:

for(int i=2;i*i<num;i++)
for(int i=2;i<=sqrt(num);i++)

这两种写法都不合适。第一种写法存在一种情况为,在num非常大的时候。ii有可能会溢出,导致ii<num,有可能会产生错误的结果;第二种写法是调用sqrt(num)会花费比较多的时间,因而建议的写法是:

for(int i=2;i<=n/i;i++)

具体代码如下:

//试除法判定质数
#include<iostream>
using namespace std;
int n;
int main(){
    cin>>n;
    while(n--){
        int num;
        cin>>num;
        if(num<2){
            cout<<"No"<<endl;
            continue;
        }
        else{
            int flag=true;
            for(int i=2;i<=num/i;i++){
                if(num%i==0){
                    cout<<"No"<<endl;
                    flag=false;
                    break;
                }
            }
            if(flag) cout<<"Yes"<<endl;
        }
    }
}

对于小于2的数直接判定为不是质数,输出No并且判断退出本次循环判定下一个数;当num大于2时,先默认是质数,将flag变量置于true,在遇到可以整除的i时,将flag置于false,并且退出本次对i的遍历。

1.3 算法时间复杂度

试除法判断质数的时间复杂度为O(sqrt(n))

2. 分解质因数

2.1 算法描述

对于任意一个数,都可以写成质数幂的乘积。分解质因数就是求出所有的底数和指数。

代码实现同样很简单,也是试除法,对于给定的数字num,遍历从2到sqrt(num)的所有数,若存在i使得num%i==0,找到了底数i,而后只需要求它的指数,num=num/i,并且用变量s来维护除以i的次数,即s为底数i的指数。

我们会发现,看起来代码将从2到sqrt(num)的所有数都试了一遍,但事实上合数并不会满足num%i=0的条件。举个例子,对于36,在遍历到6的时候,若此时还能够满足num%6=0,那最起码我还可以整除2,可以整除3,那在求质数2的时候就没有除干净。总之,满足条件的i都会是质数

另外,我们还需要注意到一个性质,对于任意的一个数num,最多只会有一个大于sqrt(num)的质因子,因而我们在让i遍历到i<=num/i结束时,若此时num不为1,那么此时num的值就是大于sqrt(1)的最后一个质因子,且指数是1.

2.2代码实现

#include<iostream>
using namespace std;
int n;
int main(){
    cin>>n;
    while(n--){
        int num;
        cin>>num;
        for(int i=2;i<=num/i;i++){
            if(num%i==0){//如果可以被这个质数除尽
                int s=0;//s记录次数
                while(num%i==0){//除不尽了就退出循环
                    num=num/i;
                    s++;
                }
                cout<<i<<" "<<s<<endl;
            }
        }
        if(num>1) cout<<num<<" "<<1<<endl;
        cout<<endl;
    }
}

2.3时间复杂度分析

试除法分解质因数的时间复杂度近似为O(n)。

3 筛质数

3.1算法描述

对于一个给定的数组num,筛质数就是要找出所有小于num的质数。基本步骤就是,遍历从2到num的所有数,若i是质数,首先将其加入存放质数的数组prime中,再将小于num且是i的倍数的数字剔除掉。

3.2 代码实现

这里我们用str[N]来记录某个数字是否被我剔除掉了。若str[i]=false,说明在2~i-1中所有的质数的若干倍并没有包含i,那么i一定是质数,可以将其加入数组prime[]中,并且用for循环将i的倍数都剔除掉

#include<iostream>
using namespace std;
const int N=1000010;
bool str[N];//来存储是否被“剔除”
int prime[N];//存放质数
int cnt;//质数的个数
int main(){
    int n;
    cin>>n;
    for(int i=2;i<=n;i++){
        if(!str[i]){
            prime[cnt++]=i;
        }
        for(int j=i+i;j<=n;j=j+i) str[j]=true;
    }
    cout<<cnt;
}

用该种算法时间复杂度:在将所有2的倍数删除时,所用的时间为n/2;将3所有的倍数删除时,所用的时间为n/3;因而对于for循环总共花费的时间为:T=n/2+n/3+n/4+……+n/n=n(1/2+1/3+1/4+……1)=nln(n),所以时间复杂度为O(nln(n))。

3.3算法优化

在上述实现过程中,对于小于n的所有数,都要执行一个比较耗时的操作:

for(int j=i+i;j<=n;j=j+i) str[j]=true;

但事实上,我们只要删除质数的倍数即可,对于一个合数p,也就是说在2~p-1,存在整数q可以整除,那这个时候在i=q的时候,p已经被剔除掉了,因而我们可以将这个语句放到if(!str[i])中,只剔除质数的倍数。

修改后的代码为:

#include<iostream>
using namespace std;
const int N=1000010;
bool str[N];//来存储是否被“剔除”
int prime[N];//存放质数
int cnt;//质数的个数
int main(){
    int n;
    cin>>n;
    for(int i=2;i<=n;i++){
        if(!str[i]){
            prime[cnt++]=i;
            for(int j=i+i;j<=n;j=j+i) str[j]=true;
        }
    }
    cout<<cnt;
}

现分析在优化算法后的时间复杂度:首先1~n中会有 n/ln(n)个质数,所以时间复杂度为O(nln(n)/ln(n)=O(n)

事实上,用朴素筛质数的算法,执行时间为:
在这里插入图片描述
优化后的筛质数的算法:
在这里插入图片描述

3.4线性筛质数

在3.3中,尽管对朴素筛质数做出了优化,但仍然有合数被重复标记,也就是说,也许某个合数本来就是true了,再给其赋予true。例如15,既是3的5倍,也是5的3倍,重复标记。

因而下给出线性筛质数的策略,保证每一个合数只会被它的最小的质因子与某个数的乘积删除。具体代码为:

for (int i = 2; i <= n; i ++ )
    {
        if (!st[i]) primes[cnt ++ ] = i;
        for (int j = 0; primes[j] <= n / i; j ++ )
        {
            st[primes[j] * i] = true;
            if (i % primes[j] == 0) break;
        }
    }

首先我们关注st[primes[j] * i] = true;这里就保证了每个合数只会被它的最小质因子删除。primes是从质数数组取出来的某个质数,分为两种情况:

1.primes[j]是i的最小质因子,那么primes[j]也一定是primes[j]*i的最小质因子,因为primes[j]是质数呀,所以primes[j]*i最多只能整除primes[j]

2.primes[j]不是i的最小质因子,说明primes[j]一定会小于i的最小质因子,这时候primes[j]仍然为primes[j]*i的最小质因子,首先primes[j]一定是primes[j]*i的质因子,假设i的最小质因子是q,那下一个primes[j]*i可以整除的数应该是primes[j]*q大于primes[j]。

综上,st[primes[j] * i] = true 将合数primes[j]*i用它的最小质因子的倍数剔除了。

那怎么可以保证不被重复标记?注意if (i % primes[j] == 0) break语句,如果i可以整除primes[j],也就是说后续i * primes[j+1]可以不用做了,因为primes[j]是i的最小质因子,说明i一定是个合数,这样无论i乘以什么数,都是primes[j]的倍数,假设这个倍数是q把,那i在遍历到q的时候,那str[primes[j]*i]=true 又会把它剔除掉。那我们会想,会不会有比primes[j]更小的质数p,使得q可以整除p,导致primes[j]*q没有被标记到呢

显然是不会的,因为iprimes[j+1]=qprimes[j],那qprimes[j]的最小质因子仍然是i的最小质因子(primes[j])为质数,所以iprimes[j+1]也就是i遍历到q的时候,p就是primes[j],因此一定会被标记。

综上,在i可以整除primes[j]时,可以退出循环。

最后一个问题,为什么循环条件为:primes[j] <= n / i 而不是j<=cnt,这是因为我总是可以保证在j=cnt前我就可以退出循环。

对于遍历到了i,如果i本身就是质数,那么由于先前!str[i]的判断,i已经加入了primes数组,那么遍历到primes[j]==i时可以退出。

如果i是合数,但是我已经把2~i-1的所有质数都加入到primes数组了,i的最小质因子肯定在primes数组中,因而也是可以退出循环的。

每个数都只筛一次,因此是线性的。

我们可以比较一下线性筛法和优化后的朴素筛法(埃氏筛法)的时间复杂度:
在这里插入图片描述
时间为 36ms,其实与埃氏筛法不会相差太多,但是我们将数据从10的6次方修改到到10的7次方:

线性筛法:

在这里插入图片描述
埃氏筛法:

在这里插入图片描述
可以看到,线性筛法的时间为141ms,埃氏筛法的时间为354ms,线性筛法的时间是可以节省的。

感谢AcWing平台

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值