题目描述

题目来源于 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. PHP addcslashes() 函数

    实例 在字符 "W" 前添加反斜杠: <?php 高佣联盟 www.cgewang.com$str = addcslashes("Hello World!" ...

  2. layui实现图片上传

    页面代码: <style> .uploadImgBtn2{ width: 120px; height: 92px; cursor: pointer; position: relative; ...

  3. 网络滴神,TCP!

     TCP在网络协议(网络协议见这篇文章)中的重要性就相当于女朋友对于程序员的重要一样,这么说你应该知道有多重要了吧. 1. 三次握手 TCP在进行数据的传输之前必须先建立连接,建立之后才能进行数据的传 ...

  4. Blob分析之ball_seq.hdev

    * ball_seq.hdev: Inspection of Ball Bonding * 关闭更新dev_update_off ()*图像集合ImageNames := 'die/' + ['die ...

  5. stm32f407 oled iic例程,成功点亮oled屏

    写了好久好久,写好多好多代码,终于把oled给驱起来了.话不多说,直接上图,欲要例程的,可以加我微 lichenpoo

  6. Java助教工作总结

    很荣幸在步入在研究生之际,有机会能协助代老师完成面向对象程序设计(java)课程的教学工作.这也是我人生中第一次接触助教工作,好多东西不太清楚,也没经验,有什么做的不好的,还望老师同学及时指出. 上周 ...

  7. Python爬取招聘网站数据,给学习、求职一点参考

    1.项目背景 随着科技的飞速发展,数据呈现爆发式的增长,任何人都摆脱不了与数据打交道,社会对于“数据”方面的人才需求也在不断增大.因此了解当下企业究竟需要招聘什么样的人才?需要什么样的技能?不管是对于 ...

  8. MySQL百万数据查询优化

    问题来源: 在查询统计的业务中做了一个小型的每隔一分钟的统计服务,实现1分钟,5分钟,1小时,2小时,一天,三天,一月,3月,一年的级联统计.前期数据来源表数据,以及生成的统计表数据都少; 数月之后, ...

  9. 配置JDK的环境变量

    1.官网下载JDK安装包并进行安装,记住安装目录 2.安装完JDK后配置环境变量  计算机→属性→高级系统设置→高级→环境变量 3.系统变量→新建 JAVA_HOME 变量 .变量值填写jdk的安装目 ...

  10. 手把手教你NLTK WordNet使用方法

    最近看了WordNet,0基础入门,简单记下笔记.因为本身不是学自然语言处理的,好多名词不是特别清楚,现有的博客读的晕乎乎,所以重新写了这个,理清思路.除了行文中提到的链接,其他几个有用的参考链接如下 ...