Bootstrap

质数的两种筛法

筛法一:朴素筛法(埃拉托斯特尼筛法/埃氏筛法)

一、原理

从 2 开始,用每一个质数去筛掉它的倍数。例如,要筛出 1 ~ 11 中的所有质数,则初始时:

1	2	3	4	5	6	7	8	9	10	11

注意 1 既不是质数也不是合数,所以直接去掉,然后从第一个质数 2 开始,将它的所有倍数筛掉:

初始数组:	1	2	3	4	5	6	7	8	9	10	11
被筛掉的数:	  			4		6		8		10
剩余的数:    	2	3		5		7		9		11

然后,将下一个质数(3)的所有倍数筛掉:

初始数组:	2	3	4	5	6	7	8	9	10	11
被筛掉的数:             		6			9
剩余的数:	2	3 	4	5		7	8		10	11  

再依次将下一个质数5、再下一个质数7、11的所有倍数筛掉,最终,没有被筛过的数即为 1~n 中所有的质数。

二、代码实现

#include <iostream>

using namespace std;

const int N = 1e6 + 10;

bool st[N]; // 记录数字i是不是质数,true=是,false=不是

int getPrime(int n)
{
    int cnt = 0; // 质数总数
    for (int i = 2; i <= n; i++)
    {
        if (st[i]) continue;
        else
        {
            cnt++;
            // 用当前质数去筛掉它的倍数
            for (int j = 2; j <= n / i; j++) // 从当前质数的2倍开始,一直到floor(n/i)倍(下取整:比自己小的最大整数;上取整:比自己大的最小整数)
                st[j * i] = true;
        }
    }
    
    return cnt;
}

int main()
{
    int n;
    cin >> n;
    
    int res = getPrime(n);
    cout << res << endl;
    
    return 0;
}

三、时间复杂度分析

循环共执行 n ln ⁡ n \frac n {\ln n} lnnn 轮(见下方质数定理),每一轮的循环长度依次是 n/2、n/3、n/5、n/7…,故一种粗略的估计方法为:
n 2 + n 3 + n 5 + n 7 + ⋯ ≈ n ln ⁡ n ⋅ ln ⁡ n = O ( n ) \frac n 2 + \frac n 3 + \frac n 5 + \frac n 7 + \cdots \approx \frac n {\ln n} \cdot \ln n = O(n) 2n+3n+5n+7n+lnnnlnn=O(n)
但以上时间复杂度并非真实的时间复杂度,它只是一个粗略估计,真实的时间复杂度为 O ( n log ⁡ log ⁡ n ) O(n \log \log n) O(nloglogn)

注:

(1)调和级数:
1 2 + 1 3 + 1 4 + ⋯ + 1 n = n ln ⁡ n \frac 1 2 + \frac 1 3 + \frac 1 4 + \cdots + \frac 1 n = n \ln n 21+31+41++n1=nlnn
(2)质数定理:1 ~ n 中有 n ln ⁡ n \frac n {\ln n} lnnn 个质数。

筛法二:线性筛法(欧拉筛)

一、原理

  • 每个合数只会被它的最小质因子筛掉;
  • 每个合数仅会被筛一次;
  • 时间复杂度为 O ( n ) O(n) O(n)

以筛 1 ~ 20 之间的质数为例:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

i = 2, n = 20, primes[j] <= 10:
	primes = [2] ---> st[4] = true, break;

i = 3, n = 20, primes[j] <= 6:
	primes = [2, 3] ---> st[6, 9] = true, break;

i = 4, n = 20, primes[j] <= 5:
	primes = [2, 3] ---> st[8] = true, break;

i = 5, n = 20, primes[j] <= 4;
	primes = [2, 3, 5] ---> st[10, 15] = true, break;

二、代码

#include <iostream>

using namespace std;

const int N = 1e6 + 10;

bool st[N];
int primes[N];

int getPrime(int n)
{
    int cnt = 0;
    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; // i可以认为是当前每个质数的多少倍
            if (i % primes[j] == 0) break; // 保证每个数只会被它的最小质因子筛掉
        }
    }
    
    return cnt;
}

int main()
{
    int n;
    cin >> n;
    
    int res = getPrime(n);
    
    cout << res << endl;
    
    return 0;
}

三、关键代码分析

(1)st[primes[j] * i] = true

当此行代码发生时,primes[j] 一定是 primes[j] * i 的最小质因子,理由如下:

  • i % primes[j] == 0 时,由于 j 是从小到大遍历的,因此当本行代码发生时,primes[j] 一定是 i 的最小质因子,因此也一定是 primes[j] * i 的最小质因子;
  • i % primes[j] != 0 时,primes[j] 一定小于 i 的所有质因子,因此 primes[j] 一定是 primes[j] * i 的最小质因子。

综上,st[primes[j] * i] = trueif (i % primes[j] == 0) break; 这两行代码的联合使用,就保证了任何一个合数一定是被它的最小质因子 primes[j] 筛掉的。

四、时间复杂度分析

由于该算法保证每个合数只会被它的最小质因子筛掉,而每个合数的最小质因子是唯一的,因此每个数只会被筛一次,故算法的时间复杂度为 O ( n ) O(n) O(n)

试除法求约数

一、算法原理

首先有如下事实:

d ∣ n d \mid n dn,则 n d ∣ n \frac n d \mid n dnn。(即若 d d d 能整除 n n n,则 n d \frac n d dn 也能整除 n n n。)

因此求 1 ∼ n 1 \sim n 1n 的所有约数时,不必从 1 枚举到 n,只需枚举到 n \sqrt n n 即可。

二、代码实现

#include <iostream>
#include <vector> // 用vector保存最终结果
#include <algorithm> // sort()函数

using namespace std;

vector<int> get_divisors(int x)
{
    vector<int> res;
    for (int i = 1; i <= x / i; i++)
    {
        if (x % i == 0)
        {
            res.push_back(i);
            if (i != x / i) res.push_back(x / i);
        }
    }
    
    sort(res.begin(), res.end());
    
    return res;
}

int main()
{
    int n;
    cin >> n;
    
    while (n--)
    {
        int x;
        scanf("%d", &x);
        
        auto res = get_divisors(x);
        
        for (auto t : res) printf("%d ", t);
        puts("");
    }
    
    return 0;
}

三、时间复杂度

由于循环最多进行到 n \sqrt n n ,故算法的时间复杂度为 O ( n ) O(\sqrt n) O(n )

求约数个数

一、算法原理

算术基本定理:任何一个大于 1 的自然数 N N N,如果 N N N 不为质数,都可以唯一分解成有限个质数的乘积:
N = P 1 α 1 P 2 α 2 ⋯ P n α n N = P_1 ^{\alpha_1} P_2 ^{\alpha_2} \cdots P_n ^{\alpha_n} \notag N=P1α1P2α2Pnαn
这里 P 1 < P 2 < ⋯ < P n P_1 < P_2 < \cdots < P_n P1<P2<<Pn 均为质数,其诸指数 α i \alpha_i αi 是正整数。这样的分解称为 N N N 的标准分解式。

故自然数 N N N 的所有约数的个数为:
( α 1 + 1 ) ( α 2 + 1 ) + ⋯ + ( α n + 1 ) (\alpha _1 + 1)(\alpha _2 + 1) + \cdots +(\alpha _n + 1) (α1+1)(α2+1)++(αn+1)
推导: N N N 的任何一个约数都可以写成
d = P 1 β 1 P 2 β 2 … P n β n d = P_1 ^ {\beta _1} P_2 ^ {\beta _2}\ldots P_n ^ {\beta _n} d=P1β1P2β2Pnβn
的形式。其中, β 1 \beta _1 β1 可以取遍 0 ∼ α 1 0 \sim \alpha_1 0α1 之间的所有整数, β 2 \beta _2 β2 可以取遍 0 ∼ α 2 0 \sim \alpha_2 0α2 之间的所有整数,…, β n \beta_n βn 可以取遍 0 ∼ α n 0 \sim \alpha_n 0αn 之间的所有整数,由分步乘法计数原理,所有可能的组合共有 ( α 1 + 1 ) ( α 2 + 1 ) + ⋯ + ( α n + 1 ) (\alpha _1 + 1)(\alpha _2 + 1) + \cdots +(\alpha _n + 1) (α1+1)(α2+1)++(αn+1) 种,每一种不同的组合就代表了一个不同的约数,因此所有的约数共有 ( α 1 + 1 ) ( α 2 + 1 ) + ⋯ + ( α n + 1 ) (\alpha _1 + 1)(\alpha _2 + 1) + \cdots +(\alpha _n + 1) (α1+1)(α2+1)++(αn+1) 个。

最大公约数(Greatest Common Divisor):

;