题目描述

题目来源于 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 = 2 和 i = 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 var_export() 函数

    var_export() 函数用于输出或返回一个变量,以字符串形式表示.高佣联盟 www.cgewang.com高佣联盟 www.cgewang.com var_export() 函数返回关于传递给该 ...

  2. Fragment为什么须要无参构造方法

    日前在项目代码里遇到偷懒使用重写Fragment带参构造方法来传参的做法,顿生好奇,继承android.support.v4.app.Fragment而又不写无参构造方法不是会出现lint错误编译不通 ...

  3. intel:spectre&Meltdown侧信道攻击(五)—— DRAM address mapping

    前面介绍了row hammer,理论上很完美,实际操作的时候会面临很尴尬的问题:内存存储数据最小的单位是cell(就是个电容,充电是1,放电是0),无数个横着的cell组成row,无数个竖着的cell ...

  4. LeetCode 164. Maximum Gap[翻译]

    164. Maximum Gap 164. 最大间隔 Given an unsorted array, find the maximum difference between the successi ...

  5. 提交项目到码云上git的使用

    git clone .. cd 到项目目录 git branch   查看当前的所有分支 git branch shanshan    创建一个属于自己的分支 git checkout shansha ...

  6. cocos2d-x_下载游戏引擎并创建第一个项目

    我是一名小白. 下载并创建游戏项目 第一步:去官网下载cocos2d-x http://www.cocos.com/download 第二步:将安装包里边的 setup.py 拖进命令行点击回车键 , ...

  7. Navicat15安装教程

    本文内容皆为作者原创,如需转载,请注明出处:https://www.cnblogs.com/xuexianqi/p/12797170.html 一:简介 Navicat是一套快速.可靠的数据库管理工具 ...

  8. three.js 着色器材质之变量(一)

    上一篇说顶点着色器和片元着色器的皮毛,这篇郭先生说一说着色器变量,通过变量可以设置材质.先看看今天要做的如下图.在线案例请点击博客原文. 在这个案例之前,我们先复习一下着色器变量 Uniforms是所 ...

  9. CF1349F 【Slime and Sequences】part1

    由于本文过长,\(\LaTeX\) 炸了,分两篇,part2 题目描述 定义一个正整数序列为好序列,当且仅当如果某个数 \(k\) 出现过,那么一定有 \(k-1\) 在最后一个 \(k\) 的前面出 ...

  10. JS 窗口加载与定时器笔记

    bom浏览器对象模型     bom由一系列相关的对象构成并且每个对象都提供了很多方法属性     bom顶级对象是window     bom是浏览器产商在各自浏览器上定义的,兼容性差     wi ...