算法学习笔记(33)——质数

质数

一、试除法判定质数

质数的定义:若一个正整数无法被除了1和它自身之外的任何自然数整除,则称该数为质数(或素数),否则称该正整数为合数。

整个自然数集合中,质数的数量不多,分布比较稀疏,对于一个足够大的整数N,不超过N的质数大约有NlnN个,即每lnN个数中大约有一个质数。

试除法:
若一个正整数N为合数,则存在一个能整除N的数T,其中2TN

题目链接:AcWing 866. 试除法判定质数

时间复杂度:O(N)

#include <iostream>

using namespace std;

bool is_prime(int n)
{
    // 特判0和1这两个数,他们既不是质数,也不是合数
    if (n < 2) return false;
    // 从2开始依次试除,此处不使用i * i <= sqrt(n)是为了避免乘积溢出
    for (int i = 2; i <= n / i; i ++ ) {
        // 若该数能够被除了1和它自身之外的数整除,则其为合数
        if (n % i == 0)
            return false;
    }
    // 遍历结束后则证明该数只能被1和它自身整除,则其为质数
    return true;
}

int main()
{
    int n;
    cin >> n;
    while (n -- ) {
        int a;
        cin >> a;
        if (is_prime(a)) puts("Yes");
        else puts("No");
    }
    return 0;
}

二、分解质因数

算数基本定理
任何一个大于 1 的正整数都能唯一分解为有限个质数的乘积,可写作:

N=p1c1p2c2...pmcm

其中 ci 都是正整数,pi 都是质数,且满足 p1<p2<...<pm

结合质数判定的"试除法",扫描 2N 的每个数 d,如果 d 是质数,则除掉 N 里面所有的质因子 d。又由于 N 中最多只含有一个大于 N 的因子,所以最后判断 N 是否大于 1,若是则 N 即为质因子。

题目链接:AcWing 867. 分解质因数

时间复杂度:O(N)

#include <iostream>

using namespace std;

void divide(int n)
{
    for (int i = 2; i <= n / i; i ++ )
        // 若i为质因数,则要将该因子除干净
        if (n % i == 0) {
            int s = 0; // s记录该质因子的次数
            while (n % i == 0) {
                n /= i;
                s ++;
            }
            cout << i << ' ' << s << endl;
        }
    // 最多只有1个大于sqrt(n)的质因子
    if (n > 1) cout << n << ' ' << 1 << endl;
    puts("");
}

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

三、筛质数

给定一个整数 N,求出 1N 之间的所有质数,称为质数的筛选问题。

3.1 朴素筛法

任意整数 x 的倍数 2x, 3x, ...都不是质数,根据质数的定义,上述规则显然成立。则枚举 2n 的每个数,如果是质数,则将该范围内它的倍数都标记为合数。

时间复杂度:O(NlogN)

每一重循环分别会执行 n2,n3,...nn次,则

n2+n3+...+nn=n(12+13+...+1n)

又因为调和级数:

k=1n1k=lnn+γ+ϵk

其中 c 为欧拉-马歇罗尼常数,约等于0.5772(无限不循环小数),ϵk 约等于 12k,并随着 k 趋于正无穷而趋于 0。又因为对数函数真值不变时,底数越大,函数值越小,图像越靠近x轴,所以:

O(lnN)=O(logeN)<O(log2N)=O(logN)

所以我们可以将时间复杂度记作O(NlogN)

void get_primes(int n)
{
    for (int i = 2; i <= n; i ++ ) {
        // 判断是否为质数,若是质数则存起来
        if (!st[i]) primes[cnt ++] = i;
        // 不管i是质数还是合数,都将其倍数标记为合数
        for (int j = i; j <= n; j += i) st[j] = true;
    }
}

3.2 埃氏筛法(Eratosthenes 筛法)

分析朴素筛法的过程易知,在遍历2和3时,都会把6标记为合数,因此可以对其进行优化,利用所有的质数就可以标记筛除所有的合数。而对于一个足够大的整数N,不超过N的质数大约有NlnN个,则大致估计时间复杂度由朴素筛法的 O(NlnN) 变为 O(NlnN/lnN)=O(N),接近线性。

实际的时间复杂度:O(NloglogN)

void get_primes(int n)
{
    for (int i = 2; i <= n; i ++ )
        if (!st[i]) {
            primes[cnt ++] = i;
            // 利用质数筛除值为其倍数的合数
            for (int j = i + i; j <= n; j += i) st[j] = true;
        }
}

3.3 欧拉筛法(线性筛法)

优化后的埃氏筛法依然会重复标记合数,例如:12 既会被 2 又会被 3 标记,12=6212=43。线性筛法通过“从大到小累积质因子”的方式标记每一个合数,即让 12 仅有 322 这一种产生方式。使得每个合数只会被他的最小质因子筛一次,时间复杂度为 O(N)

void get_primes(int n)
{
    // 依次考虑2~N的每一个数i
    for (int i = 2; i <= n; i ++ ) {
        // 若为质数,则存起来
        if (!st[i]) primes[cnt ++] = i;
        // 扫描不大于 n/i 的每个质数(因为primes[j]*i>n时 筛除大于n的合数没有意义)
        for (int j = 0; primes[j] <= n / i; j ++ ) {
            // 利用最小质因子筛除合数
            st[primes[j] * i] = true;
            /*
             * 如果 i % primes[j] != 0
             * 代表 i 的最小质因子还没有找到,即 i 的最小质因子应该大于 primes[j]
             * 则 primes[j] 就应该是 primes[j] * i 的最小质因子
             * 所以 primes[j] * i 被 primes[j] 筛除
             *
             * 如果 i % primes[j] == 0
             * 代表 i 的最小质因子就是 primes[j]
             * 则后续的 primes[j+k] * i 应该被 primes[j] 筛除
             * 所以跳出循环,在之后的循环轮次(i更大时)筛除
             */
            if (i % primes[j] == 0) break;
        }
    }
}
posted @   S!no  阅读(171)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示