Acwing 质数

1.试除法判定质数

首先回顾一下什么是质数?

  • 对所有大于1的自然数,如果这个数的约数只包含1和它本身,则这个数被称为质数或者素数

试除法:对于一个数n,从2枚举到n-1,若有数能够整除n,则说明除了1和n本身,n还有其它约数,则n不是质数;否则,n是质数;

  • 优化:由于一个数的约数都是成对出现的。比如12的一组约数是3,4,另一组约数是2,6。则我们只需要枚举较小的那一个约数即可。我们用d|n来表示d整除n,只要满足d|n,则一定有{n/d}|n,比如3∣12,则{12/3} | 12,因为约数总是成对出现的,我们只需要枚举小的那部分数即可,令d ≤ n/d,即,d ≤ sqrt{n},因此对于n,只枚举2到sqrt{n}即可。
  • 注意:for循环的结束条件,推荐写成i <= n / i。有的人可能会写成i <= sqrt(n),这样每次循环都会执行一次sqrt函数,而这个函数是有一定时间复杂度的。而有的人可能会写成i * i <= n,这样当i很大的时候(比如i比较接近int的最大值时),i * i可能会溢出,从而导致结果错误。

Acwing 866.试除法判定质数
在这里插入图片描述
具体实现代码(详解版):

#include <iostream>  
using namespace std;

//试除法:用于判断传入的整数 x 是否为素数
bool is_prime(int x){
    // 如果 x 小于 2,则不是素数,返回 false
    if(x < 2) return false;
    
    // 从 2 开始迭代,直到 i * i <= x (相当于 i <= sqrt(x)),检查是否有因数
    for(int i = 2; i <= x / i; i++){
        // 如果 x 能被 i 整除,说明 x 不是素数,返回 false
        if(x % i == 0)
            return false;
    }
    
    // 如果没有找到任何因数,返回 true,表示 x 是素数
    return true;
}

int main(){
    int n, x;  
    cin >> n;  
    while(n --){  
        cin >> x;  
        // 如果 x 是素数,输出 "Yes",否则输出 "No"
        if(is_prime(x)) puts("Yes");
        else puts("No");
    }
    
    return 0; 
}

2.质因数分解:试除法

对于一个整数 N 总能写成如下形式:
N = P 1 α 1 × P 2 α 2 × P 3 α 3 ⋯ × P n α n N=P_{1} ^{α_1} \times P_{2} ^{α_2} \times P_{3} ^{α_3} \dots \times P_{n} ^{α_n} N=P1α1×P2α2×P3α3×Pnαn
其中Pi都是质数,αi为大于0的正整数,即一个整数可以表示为多个不同质数的次方的乘积

  • 对于一个数求质因数的过程:从2到n,枚举所有数,依次判断是否能够整除n即可。朴素法,时间复杂度O(n))。;
  • 优化:n中只包含一个大于sqrt{n}的质因子,很好证明,如果中包含两个大于sqrt{n}的质因子,那么乘起来就大于n了。因此,在枚举的时候可以先把2到sqrt{n}的质因子枚举出来,如果最后处理完n > 1,那么这个数就是那个大于\sqrt{n}的质因子,单独处理一下就可以。时间复杂度降为O(sqrt(n))

求质因数分解,为什么枚举所有数,而不是枚举所有质数,万一枚举到合数怎么办?解释:枚举数时,对于每个能整除 n 的数 i先把这个数除干净了(就是把这个质数的次方剔除了,表现在上式中就是逐步去除Pi^αi),再继续枚举后面的数,这样能保证,后续再遇到能整除的数,一定是质数而不是合数。

例如:求180的质因数分解

  1. i = 2 n = 180 / 2 = 90 / 2 = 45
  2. i = 3 n = 45 / 3 = 15 / 3 = 5
  3. i = 4 当i是合数时,i 一定不能整除 n 。如果 4 能整除 n 那么 2 一定还能整除 n,就是在 i = 2的时候没有除干净,而我们对于每个除数都是除干净的,因此产生矛盾。
  4. i = 5 n = 5 / 5 = 1

Acwing 867.分解质因数
在这里插入图片描述
具体实现代码(详解版):

#include <iostream>  

using namespace std;

// 使用试除法分解质因数
void divide(int x){
    // 从 2 开始迭代,直到 i * i <= x (相当于 i <= sqrt(x)),试图找到因数
    for(int i = 2 ; i <= x / i ; i ++){
        // 如果 x 能被 i 整除,则 i 是 x 的一个质因数
        if(x % i == 0){
            int s = 0;  // 计数 i 的出现次数
            // 用 while 循环找出 i 作为因子的次数,直到 x 不能被 i 整除
            while(x % i == 0) x /= i, s ++;
            cout << i << ' ' << s << endl;
        }
    }
    // 如果 x 最后还大于 1,说明剩下的 x 是一个大于 sqrt(x) 的质数
    if(x > 1) cout << x << ' ' << 1 << endl; 
    cout << endl;  
}

int main(){
    int n, x;  
    cin >> n;  
    while(n --){  
        cin >> x;  
        divide(x); 
    }
    
    return 0;  
}

3.筛质数–朴素法

将2到n全部数放在一个集合中,遍历2到n,每次删除当前遍历的数在集合中的倍数。最后集合中剩下的数就是质数。

解释:如果一个数p没有被删掉,那么说明在2到p-1之间的所有数,p都不是其倍数,即2到p-1之间,不存在p的约数。故p一定是质数。

时间复杂度:
n 2 + n 3 + ⋯ + n n = n ln ⁡ n < n log ⁡ 2 n \frac{n}{2} + \frac{n}{3} + \dots +\frac{n}{n} = n\ln_{}{n} < n\log_{2}{n} 2n+3n++nn=nlnn<nlog2n
故,朴素思路筛选质数的时间复杂度大约为O(nlogn)

Acwing 868.筛质数
在这里插入图片描述
具体实现代码(详解版):

#include <iostream>
using namespace std;

const int N = 1000010; 
int primes[N], cnt;     // primes 数组存储所有找到的素数,cnt 计数素数的个数
bool st[N];             // st 数组标记数字是否被筛掉,false 表示未筛掉,true 表示已筛掉

// 朴素筛法,用于筛选出小于等于 n 的所有素数
void get_prime(int n) {
    // 从 2 开始遍历到 n,逐个检查数字是否是素数
    for (int i = 2; i <= n; i++) {
        // 如果 st[i] 为 false,说明 i 没有被筛掉,因此 i 是素数
        if (!st[i]) primes[cnt++] = i;
        
        // 将所有 i 的倍数标记为 true,表示这些数不是素数
        for (int j = i + i; j <= n; j += i) {
            st[j] = true;
        }
    }
}

int main() {
    int n;
    cin >> n;  
    get_prime(n);  
    cout << cnt << endl;  
    return 0;
}

4.筛质数–埃氏筛法

在上面朴素筛法的基础上,

其实不需要把全部数的倍数删掉,而只需要删除质数的倍数即可

对于一个数p,判断其是否是质数,其实不需要把2到p-1全部数的倍数删一遍,只要删掉2到p-1之间的质数的倍数即可。因为,若p不是个质数,则其在2到p-1之间,一定有质因数,只需要删除其质因数的倍数,则p就能够被删掉。埃氏筛法筛选质数的时间复杂度大约为O{nlog(logn)}

具体实现代码(详解版):

#include <iostream>  
using namespace std;

const int N = 1000010; 
int primes[N], cnt;     // primes 数组存储所有素数,cnt 记录素数的个数
bool st[N];             // st 数组用于标记每个数是否被筛掉,false 表示未筛掉

// 埃拉托斯特尼筛法,找出小于等于 n 的所有素数
void get_prime(int n) {
    // 从 2 开始遍历,检查每个数是否是素数
    for (int i = 2; i <= n; i++) {
        // 如果 i 没有被筛掉,则 i 是一个素数
        if (!st[i]) {
            primes[cnt++] = i;  // 将素数存储到 primes 数组中,并增加素数个数计数
            // 将 i 的所有倍数标记为非素数,从 i * 2 开始,每次增加 i
            for (int j = i + i; j <= n; j += i)
                st[j] = true;  // 标记 i 的倍数为 true,表示它们不是素数
        }
    }
}

int main() {
    int n;
    cin >> n;  // 输入一个整数 n,表示筛选范围为 2 到 n
    get_prime(n);  // 调用 get_prime 函数进行素数筛选
    cout << cnt << endl;  // 输出素数的个数
    return 0;
}

5.筛质数–线性筛法

大体思路和埃氏筛法一样,将合数用他的某个因数筛掉,其性能要优于埃氏筛法(在 1 0 6 10^{6} 106下两个算法差不多,在10^7下线性筛法大概快一倍)核心思路是:对于某一个合数n,其只会被自己的最小质因子给筛掉,从而避免了重复标记

设置一个primes数组,存储质数(以下叙述用pj来表示primes[j]),从2到n进行循环遍历,用数组st[]标记是否为质数。每次循环都对当前质数数组进行遍历,用其最小质因子筛除合数

  • i % pj == 0时:pj 一定是 i 的最小质因子,因为我们是从小到大枚举质数的,首先遇到的满足i % p j == 0的,pj 一定是 i 的最小质因子并且pj 一定是pj * i的最小质因子。比如,15 = 3 *5,15的最小质因子是3,则15的倍数中最小的数,其最小质因子同样是3的,15乘以最小质因子3,即45;
  • i % pj != 0时:pj 一定不是 i 的质因子,并且由于是从小到大枚举质数的,那么 pj 一定小于 i 的全部质因子。那么 pj 就一定是 pj * i 的最小质因子

具体实现代码(详解版):

#include <iostream>
using namespace std;

const int N = 1000010;  
int primes[N], cnt;     // primes 数组存储所有找到的素数,cnt 记录素数的个数
bool st[N];             // st 数组标记数字是否被筛掉,false 表示未筛掉

// 线性筛法,筛选出小于等于 n 的所有素数
void get_prime(int n) {
    // 遍历 2 到 n,逐个判断每个数是否为素数
    for (int i = 2; i <= n; i++) {
        // 如果 st[i] 为 false,则 i 是素数,将其存入 primes 数组
        if (!st[i]) primes[cnt++] = i;
        
        // 遍历所有已找到的素数 primes[j]
        for (int j = 0; primes[j] <= n / i; j++) {
            // 将 i 与当前素数 primes[j] 的乘积标记为合数
            st[primes[j] * i] = true;
            
            // 如果 i 是 primes[j] 的倍数,停止筛选,因为后面的乘积已经包含该素数
            if (i % primes[j] == 0) break;
        }
    }
}

int main() {
    int n;
    cin >> n; 
    get_prime(n);  
    cout << cnt << endl; 
    return 0;
}

对比总结:

筛法名称时间复杂度空间复杂度算法思想优缺点
朴素筛法O(n√n)O(1)对于每个数 i,依次检查它是否是素数,若是素数,将其倍数标记为合数。优点:实现简单,代码直观。缺点:时间复杂度较高,筛选效率低。
埃拉托斯特尼筛法O(n log log n)O(n)从 2 开始,对于每个素数 i,将它的所有倍数标记为合数。优点:效率比朴素筛法高。缺点:每个合数可能被多次标记(即被多个素数筛掉)。
线性筛法O(n)O(n)对每个数 i,只用它的最小质因子来筛选它的倍数,保证每个合数只被标记一次。优点:筛选效率最高,时间复杂度接近线性。缺点:实现稍复杂,理解难度较高。

以上基本可以解决所有素数的问题,多写几遍板子。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值