质数相关问题

判断质数

如果一个正整数的因子只有1和他本身,那么该数为质数。质数必须大于1。因此我们可以对任意数n,从2到n-1穷举,如果这期间发现n的其他因子,那么该数就不是质数。

一个数被拆分成两个因子,如6=2* 3,那么只需要穷举2,不需要再穷举3,拆分成的两个因子是成对的。因此可以优化,只需要穷举到sqrt(n)即可。时间复杂度固定为O(sqrt(n))。

例题:https://www.acwing.com/problem/content/868/

#include <iostream>
#include <math.h>

using namespace std;

int n;

// 最优写法
bool is_prime(int x) {
    if (x < 2) return false;
    
    for (int i = 2; i <= x / i; i ++ ) {
        if (x % i == 0) return false;
    }
    
    return true;
}

// 借助sqrt函数
bool is_prime2(int x) {
    if (x < 2) return false;
    
    int q = sqrt(x);
    
    for (int i = 2; i <= q; i ++ ) {
        if (x % i == 0) return false;
    }
    
    return true;
}

// 不推荐,当i过大时,i*i会溢出变为负数,导致结果出错
bool is_prime3(int x) {
    if (x < 2) return false;
    
    for (int i = 2; i * i <= x; i ++ ) {
        if (x % i == 0) return false;
    }
    
    return true;
}

int main() {
    
    scanf("%d", &n);
    
    while (n -- ) {
        int x;
        scanf("%d", &x);
        
        if (is_prime(x)) printf("Yes\n");
        else printf("No\n");
    }
    
    return 0;
}

分解质因数

对于一个大于2的正整数,可以被分解为质数相乘的形式,比如100=2* 2* 5* 5,2和5也叫做质因数。想要获取质因数,依旧可以穷举,当n可以被2整除就一直对它除2(期间记录可以除多少个2),当n可以被3整除就一直对它除3...直到穷举到sqrt(n)。

该方法的正确性源于,使用2除尽后,n就不可能再被诸如4、8这类数整除,以此类推。该算法时间复杂度介于 O(logn)-O(sqrt(n))

例题:https://www.acwing.com/problem/content/869/

#include <iostream>

using namespace std;

int n;

void func(int x) {
    
    for (int i = 2; i <= x / i; i ++ ) {
        
        int cnt = 0;
        
        while (x % i == 0) {
            x /= i;
            cnt ++ ;
        }
        
        if (cnt) printf("%d %d\n", i, cnt);
    }
    
    // 在最后会由于 i <= x / i 导致剩余一个质数
    if (x != 1) printf("%d %d\n", x, 1);
    
    printf("\n");
}

int main() {
    
    scanf("%d", &n);
    
    while (n -- ) {
        int x;
        scanf("%d", &x);
        
        func(x);
    }
    
    return 0;
}

筛质数

筛质数就是将1-n中的质数筛选出来。

例题:https://www.acwing.com/problem/content/870/

埃氏筛

当一个数为质数时,将由该质数组成的合数筛掉,例如2是质数,那么4、6、8、10...都是以2为质因数的合数,因此他们都将被筛掉。之后的3作为质数也重复这一操作...埃氏筛时间复杂度为O(nloglogn)

#include <iostream>

using namespace std;

const int N = 1000010;

int prime[N];

bool st[N];

void ai_screen(int x) {
    
    int cnt = 0;
    
    for (int i = 2; i <= x; i ++ ) {
        
        // i是质数,将以i为因子的合数筛掉
        if (!st[i]) {
            
            // 记录质数
            prime[cnt ++ ] = i;
            
            // 筛掉合数
            for (int j = i; j <= x; j += i) {
                st[j] = true;
            }
        }
    }
    
    printf("%d", cnt);
}

int main() {
    
    int n;
    scanf("%d", &n);
    
    ai_screen(n);
    
    return 0;
}

问题主要在于,如何保证遍历过程中,遇到的数一定是质数?
做不严谨的个证明:假设遇到数为p,那么如果p为合数,一定会被从2到p-1的质数筛选掉(从2到p-1一定存在p的质因子),因此p一定是质数。

线性筛

在埃氏筛中可以发现,一个数会被重复的筛掉,例如6会被2和3重复筛,12会被2、3、4重复筛... 因此优化的地方在于,我们期望一个合数只会被它的最小质因子筛掉,即12被2筛过后,就不会再被3、4、6筛选。

换句话说就是在遇到2的时候,只将以2作为最小质因数的合数筛掉,例如2、4、6、8、10、12...遇到3的时候只将以3作为最小质因数的合数筛掉,例如9、15...

#include <iostream>

using namespace std;

const int N = 1000010;

int prime[N];

bool st[N];

void linear_screen(int x) {
    
    int cnt = 0;
    
    for (int i = 2; i <= x; i ++ ) {
        
        if (!st[i]) prime[cnt ++ ] = i;
        
        // 从小到大枚举已知的质数
        for (int j = 0; prime[j] <= x / i; j ++ ) {

            // prime[j]是i的最小质因数,
            // 因此prime[j]也一定是prime[j] * i的最小质因数
            if (i % prime[j] == 0) {
                
                st[prime[j] * i] = true;
                break;
            }
            
            // prime[j]不是i的质因数,但是prime[j]一定小于i的最小质因数,
            // 因此prime[j]也一定是prime[j] * i的最小质因数
            else {
                
                st[prime[j] * i] = true;
            }
        }
    }
    
    printf("%d", cnt);
}

int main() {
    
    int n;
    scanf("%d", &n);
    
    linear_screen(n);
    
    return 0;
}

举例来说明算法流程:
1、从2开始枚举,将2作为质数存储,2%2=0,因此4将被筛掉
2、枚举到3,3作为质数存储,3%2!=0,因此6倍筛掉;3%3=0,因此9被筛掉
3、枚举到4,4不是质数,4%2=0,因此8被筛掉
4、枚举到5,5作为质数存储,5%2!=0,因此10被筛掉;5%3!=0,因此15被筛掉
5、枚举到6,6不是质数,6%2=0,因此12被筛掉
6、...

往后列举可以发现,每个合数都是被它的最小质因子筛掉的。i在枚举过程中,不断的和已确定的质数相乘,筛掉后面未枚举到的合数。已确定的质数也是按从小到大的顺序排列的,因此可以确保使用最小质因子筛选合数。

当i可以被某个质因子x整除时,i不能再与比x大的质因子相乘进行筛选,因为这过多的操作会在i枚举到其他数时出现重复。比如i=6,在与2相乘后就不再继续和其他质因子相乘。如果我们硬要相乘,那么6* 3=18将被筛掉。在i=9时,将会重复的执行9* 2=18,18被筛了两次。

将上文代码精简,可以得到如下代码:

void linear_screen(int x) {
    
    int cnt = 0;
    
    for (int i = 2; i <= x; i ++ ) {
        
        if (!st[i]) prime[cnt ++ ] = i;
        
        // 从小到大枚举已知的质数
        for (int j = 0; prime[j] <= x / i; j ++ ) {

            // prime[j]是i的最小质因数,
            // 因此prime[j]也一定是prime[j] * i的最小质因数
            
            // prime[j]不是i的质因数,但是prime[j]一定小于i的最小质因数,
            // 因此prime[j]也一定是prime[j] * i的最小质因数
            
            st[prime[j] * i] = true;
            if (i % prime[j] == 0) break;
        }
    }
    
    printf("%d", cnt);
}

对于循环退出条件prime[j] <= x / i可以变换为prime[j] * i <= x,目的是防止相乘后要筛掉的值大于n,防止做无用筛选。由乘法换为除法是为了防止相乘导致溢出。

另外,j不需要判断越界(j<cnt),原因有2:
1、当i为质数,i会被计入prime数组,随着遍历prime数组,i到最后会和自己整除,进而退出循环
2、当i为合数,i在遍历prime数组过程中,就将会被某个质数作为因子整除,进而退出循环

posted @ 2022-03-03 16:56  moon_orange  阅读(173)  评论(0编辑  收藏  举报