题目描述

题目来源于 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 is_float()、 is_double()、is_real()函数

    is_float() 函数用于检测变量是否是浮点型. 别名函数:is_double(),is_real().高佣联盟 www.cgewang.com 注意: 若想测试一个变量是否是数字或数字字符串(如 ...

  2. PHP ucfirst() 函数

    实例 把 "hello" 的首字符转换为大写: <?phpecho ucfirst("hello world!");?> 运行实例 » 定义和用法 ...

  3. Virtuoso 中 display.drf、techfile.tf、tech.db 之间的关系,以及 Packet 在它们之间的作用

    https://www.cnblogs.com/yeungchie/ 一般工艺库下的"技术文件"有 tech.db 和 techfile.tf , Packet 是 display ...

  4. linux的服务管理(centos6和Centos7)和网络管理(网卡配置),计划服务cron

    服务和网络 管理 init  ifcfg ens33 1.服务: Linux系统中提供的功能,统称为服务,如:at服务.cron服务.web服务.FTP服务.sshd服务等. 服务是由已经在运行的进程 ...

  5. java开发-flyway

    数据库版本管理工具 什么是数据库版本管理? 做过开发的小伙伴们都知道,实现一个需求时,一般情况下都需要设计到数据库表结构的修改.那么我们怎么能保证项目多人开发时,多个数据库环境(测试,生产环境)能够保 ...

  6. ThreadLocal面试六连问

    转自:码农沉思录 中高级阶段开发者出去面试,应该躲不开ThreadLocal相关问题,本文就常见问题做出一些解答,欢迎留言探讨. ThreadLocal为Java并发提供了一个新的思路, 它用来存储T ...

  7. 薪资高,福利好,会Python的人就是这么豪横!

    很多人可能会有这样的疑问,数据分析Excel挺强大的,会Excel就行,为什么还要去学python? 是的,Excel和python对于数据分析而言,这两者都只是不同的工具而已. 很多人学习pytho ...

  8. 能动手绝不多说:开源评论系统remark42上手指南

    能动手绝不多说:开源评论系统 remark42 上手指南 前言 写博客嘛, 谁不喜欢自己倒腾一下呢. 从自建系统到 Github Page, 从 Jekyll 到 Hexo, 年轻的时候谁不喜欢多折腾 ...

  9. js 自定义属性操作

    自定义属性操作     element.属性 获取内置属性值     element.getAttribute("属性") 我们自己添加的属性叫自定义属性     element. ...

  10. 经典的IPC问题

    Inter-Process Communication的缩写,含义是进程间通信,是指两个进程间交换数据的过程. 哲学家进餐问题 概述 哲学家进餐/思考 进餐需要两把叉子 每次拿一把叉子 如何预防死锁 ...