题目描述

题目来源于 LeetCode 204.计数质数,简单来讲就是求“不超过整数 n 的所有素数个数”

常规思路

一般来讲,我们会先写一个判断 a 是否为素数的 isPrim(int a) 函数:

bool isPrim(int a){
for (int i = 2; i < a; i++)
if (a % i == 0)//存在其它整数因子
return false;
return true;
}

然后我们会写一个 countIsPrim(int n) 来计算不超过 n 的所有素数个数:

int countPrimes(int n) {
int ans = 0;
for (int i = 2; i < n; i++)
if (isPrim(i)) ans++;
return ans;
}

显然这两个嵌套的 for 循环时间复杂度是 \(O(n^2)\) ,但是这样写有两个主要的问题:

  1. isPrim() 函数的计算冗余。首先,举个例子引入一下因子的对称性

    12 = 2 × 6
    12 = 3 × 4
    12 = sqrt(12) × sqrt(12)
    12 = 4 × 3
    12 = 6 × 2

    所以当循环判断一个数 \(a\) 是否有除了 1 和它本身之外其余的因子时,我们只需要将循环变量终止在 \(\sqrt a\) 的位置,而非 [2, a) 的所有数。

  2. countPrimes(int n)函数的计算冗余。例如,一旦我们判断2为质数那么所有2的倍数一定都是质数,如果我们知道3是质数,那么所有3的倍数也一定都是质数。所以如果我们将 [2, n) 的数都进行一次 isPrim() ,那么将带来巨大的时间浪费。

我们在这里,先将第一个问题解决,即优化 isPrim() 函数:

bool isPrim(int n){
//根据因子对称性
for (int i = 2; i * i <= n; i++){
if (n % i == 0)//存在其它整数因子
return false;
}
return true;
}

采用 Sieve of Eratosthenes 算法高效实现

这个算法的中文叫作“埃拉托斯特尼筛法”,听起来很复杂,但是并不难理解,本质上就是把常规思路反过来,如下面动图所示:

下面我们逐渐引出该算法的全貌:

常规思路就是将区间为 [2, n) 的数都遍历一遍,在过程中累加素数的个数。上述问题二已经说明了其低效性,根据“如果 i 是质数,那么所有 i 的倍数都不是质数”,我们做出优化:

int countPrimes(int n) {
vector<int> IsPrim(n + 1, true);
for (int i = 2; i < n; i++){
if (isPrim[i]){
//如果i是质数,那么所有i的倍数都不是质数
for (int j = 2 * i; j < n; j += i){
IsPrim[j] = false;
}
}
}
//遍历一遍计算结果
int ans = 0;
for (int i = 2; i < n; i++){
if (IsPrim[i]) ans++;
}
return ans;
}

这段代码展现了该算法的整体思路,但是还有两个细节可以优化:

  1. 由于因子的对称性,我们可以将外层 for 循环改为:for (int i = 2; i * i < n; i++)
  2. 将内层循环改为:for (int j = i * i; j < n; j += i) 。举个例子, n = 25 ,当 i = 4 时算法会标记 4 × 2 = 8,4 × 3 = 12 等等数字,但是这两个数字已经被 i = 2i = 3 的 2 × 4 和 3 × 4 标记了。所以我们可以从平方项开始遍历。

到这里,Sieve of Eratosthenes 算法就已经实现了,下面给出完整的代码:

int countPrimes(int n) {
vector<int> prims(n + 1, 1);
for (int i = 2; i * i < n; i++){
if (isPrim[i]){
//如果i是质数,那么所有i的倍数都不是质数
for (int j = i * i; j < n; j += i){
prims[j] = 0;
}
}
}
//遍历一遍计算结果
int ans = 0;
for (int i = 2; i < n; i++){
if(prims[i]) ans++;
}
return ans;
}

Sieve of Eratosthenes 算法的证明

该算法的时间复杂度为 \(O(nloglogn)\) ,下面给出三个公式和证明:

\(Prerequisite\).

调和级数(Harmonic series)是一个发散的无穷级数,当 \(n\) 趋近于无穷大时,有一个近似公式:

\[1+\frac{1}{2}+\frac{1}{3}+\cdot\cdot\cdot\frac{1}{n} = \sum_{i=1}^{n}\frac{1}{i} = ln(n) + \gamma
\]

其中 \(\gamma\) 为欧拉常数,\(\gamma \approx 0.57721\)

泰勒级数(Taylor series)是1715年英国数学家布鲁克·泰勒提出的,在零点的导数求得的泰勒级数又叫麦克劳林级数,一个常用的泰勒级数如下:

\[ln\frac{1}{1-x} = \sum^{\infty}_{n=1}\frac{x^n}{n} = x + \frac{x^2}{2} + \frac{x^3}{3} + \cdot\cdot\cdot+\frac{x^n}{n}
\]

对任意 \(x \in [-1,1)\) 都成立。

欧拉乘积公式(Euler product)是著名的瑞士数学家欧拉于1737年在俄罗斯的圣彼得堡科学院发表的重要公式,为数学家研究素数的分布奠定了基础,即:

\[\sum_{n}^{}\frac{1}{n^{s}} = \prod_{p}^{1}\frac{1}{1-p^{-s}}
\]

其中 \(n\) 是自然数,\(p\) 为素数。


\(Prove.\)

该算法的运行时间可以看作筛除的次数之和

\[\frac{n}{2}+\frac{n}{3}+\frac{n}{5}+ \cdot \cdot \cdot +\frac{n}{p} = n \cdot (\frac{1}{2}+\frac{1}{3}+\frac{1}{5}+ \cdot \cdot \cdot +\frac{1}{p}) = n \sum_{p}\frac{1}{p}
\]

显然我们需要想办法处理后面的质数倒数和。我们拿出欧拉乘机公式,将所有的 \(s\) 用1来代替:

\[\sum_{n}^{}\frac{1}{n} = \prod_{p}^{1}\frac{1}{1-p^{-1}}
\]

两侧同时取对数:

\[ln(\sum_{n}^{}\frac{1}{n}) = \sum_{p}^{}ln\frac{1}{1-p^{-1}}
\]

由于 \(-1< p^{-1} < 1\) ,所以对上面右侧求和的每一项进行泰勒展开得到:

\[ln\frac{1}{1-p^{-1}} = \sum^{\infty}_{n=1}\frac{1}{np^{n}} = \frac{1}{p} + \frac{1}{2p^2} + \frac{1}{3p^3} + \cdot\cdot\cdot + \frac{1}{np^n}
\]

故得到:

\[
\begin{equation}
\begin{split}
ln(\sum_{n}^{}\frac{1}{n}) &= \sum_{p}^{}\frac{1}{p} + \sum_{p}^{}\frac{1}{p^2}(\frac{1}{2} + \frac{1}{3p} + \frac{1}{4p^2}+\cdot\cdot\cdot)\\
&< \sum_{p}^{}\frac{1}{p} + \sum_p\frac{1}{p^2}(1+\frac{1}{p}+\frac{1}{p^2}+\cdot\cdot\cdot)\\
&= \sum_{p}^{}\frac{1}{p}+\sum_p\frac{1}{p(p-1)}\\
&= \sum_p\frac{1}{p} + C
\end{split}
\end{equation}
\]

上式左侧带入调和级数,当 \(n\) 趋向于无穷时得到:

\[\sum_{p}^{}\frac{1}{p} = ln(ln(n))
\]

到这里就成功处理掉了质数倒数和,所以时间复杂度为 \(O(nloglogn)\) 。

耗时比较

未改进版 \(isPrim()\) 、改进版 \(isPrim()\) 、高效算法三者耗时对比如下,可以看出来差距还是很大的:





参考资料

  1. 埃拉托斯特尼筛法

  2. 这个大概是唯一一个证明了时间复杂度的题解了!~

  3. 如何高效寻找素数

  4. Divergence of the sum of the reciprocals of the primes

  5. 素数的倒数之和

“计数质数”问题的常规思路和Sieve of Eratosthenes算法分析的更多相关文章

  1. [LeetCode] 204. Count Primes 计数质数

    Description: Count the number of prime numbers less than a non-negative number, n click to show more ...

  2. Leecode刷题之旅-C语言/python-204计数质数

    /* * @lc app=leetcode.cn id=204 lang=c * * [204] 计数质数 * * https://leetcode-cn.com/problems/count-pri ...

  3. Leetcode 204计数质数

    计数质数 统计所有小于非负整数 n 的质数的数量. 示例: 输入: 10 输出: 4 解释: 小于 10 的质数一共有 4 个, 它们是 2, 3, 5, 7 . 比计算少n中素数的个数. 素数又称质 ...

  4. Java实现 LeetCode 204 计数质数

    204. 计数质数 统计所有小于非负整数 n 的质数的数量. 示例: 输入: 10 输出: 4 解释: 小于 10 的质数一共有 4 个, 它们是 2, 3, 5, 7 . class Solutio ...

  5. [原]素数筛法【Sieve Of Eratosthenes + Sieve Of Euler】

    拖了有段时间,今天来总结下两个常用的素数筛法: 1.sieve of Eratosthenes[埃氏筛法] 这是最简单朴素的素数筛法了,根据wikipedia,时间复杂度为 ,空间复杂度为O(n). ...

  6. 埃拉托色尼筛法(Sieve of Eratosthenes)求素数。

    埃拉托色尼筛法(Sieve of Eratosthenes)是一种用来求所有小于N的素数的方法.从建立一个整数2~N的表着手,寻找i? 的整数,编程实现此算法,并讨论运算时间. 由于是通过删除来实现, ...

  7. algorithm@ Sieve of Eratosthenes (素数筛选算法) & Related Problem (Return two prime numbers )

    Sieve of Eratosthenes (素数筛选算法) Given a number n, print all primes smaller than or equal to n. It is ...

  8. 使用埃拉托色尼筛选法(the Sieve of Eratosthenes)在一定范围内求素数及反素数(Emirp)

    Programming 1.3 In this problem, you'll be asked to find all the prime numbers from 1 to 1000. Prime ...

  9. Sieve of Eratosthenes时间复杂度的感性证明

    上代码. #include<cstdio> #include<cstdlib> #include<cstring> #define reg register con ...

随机推荐

  1. NodeJS 极简教程 <1> NodeJS 特点 & 使用场景

    NodeJS 极简教程 <1> NodeJS 特点 & 使用场景 田浩 因为看开了所以才去较劲儿.   1. NodeJS是什么 1.1 Node.js is a JavaScri ...

  2. 7.18 NOI模拟赛 树论 线段树 树链剖分 树的直径的中心 SG函数 换根

    LINK:树论 不愧是我认识的出题人 出的题就是牛掰 == 他好像不认识我 考试的时候 只会写42 还有两个subtask写挂了 拿了37 确实两个subtask合起来只有5分的好成绩 父亲能转移到自 ...

  3. 2020牛客暑假多校训练营 第二场 G Greater and Greater bitset

    LINK:Greater and Greater 确实没能想到做法. 考虑利用bitset解决问题. 做法是:逐位判断每一位是否合法 第一位 就是 bitset上所有大于\(b_1\)的位置 置为1. ...

  4. 6.18 省选模拟赛 树 倍增 LCT

    LINK:树 考虑暴力 保存每个版本的父亲 然后暴力向上跳.得分20. 考虑离线 可以离线那么就可以先把树给搞出来 然后考虑求k级祖先 可以倍增求. 如何判断合法 其实要求路径上的边的时间戳<= ...

  5. 4.23 子串 AC自动机 概率期望 高斯消元

    考虑40分. 设出状态 f[i]表示匹配到了i位还有多少期望长度能停止.可以发现这个状态有环 需要高斯消元. 提供一种比较简单的方法:由于期望的线性可加性 可以设状态f[i]表示由匹配到i到匹配到i+ ...

  6. MySQL-安装配置篇

    一.MySQL二进制安装包安装 1.环境初始化 1)创建目录mkdir /app/database --安装路径 mkdir /data/3306 --存放数据路径 mkdir /binlog/330 ...

  7. 数据分析second week(7.22~7.28)

    描述性统计Python实现 这周学习时间也就几个小时,由于python也正在学习,Anaconda也有,所以那些安装啥的就偷懒下不写了,直接贴出python代码 数据是随机生成,计算是调用库里的函数. ...

  8. 020_go语言中的接口

    代码演示 package main import ( "fmt" "math" ) type geometry interface { area() float ...

  9. 比PS还好用!Python 20行代码批量抠图

    你是否曾经想将某张照片中的人物抠出来,然后拼接到其他图片上去,从而可以即使你在天涯海角,我也可以到此一游? 很多人学习python,不知道从何学起.很多人学习python,掌握了基本语法过后,不知道在 ...

  10. Python3,逻辑运算符

    优先级 ()>not>and>or 1.or 在python中,逻辑运算符or,x or y, 如果x为True则返回x,如果x为False返回y值.因为如果x为True那么or运算 ...