题目描述

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

    实例 计算并返回数组的乘积: <?php$a=array(5,5);echo(array_product($a));?> 运行实例 » 定义和用法 array_product() 函数计算 ...

  2. PHP registerXPathNamespace() 函数

    实例 为下一个 XPath 查询创建命名空间上下文: <?php$xml=<<<XML高佣联盟 www.cgewang.com<book xmlns:chap=" ...

  3. 牛客挑战赛40 VMware和基站 set 二分 启发式合并 区间覆盖

    LINK:VMware和基站 一道 做法并不常见的题目 看起来很难写 其实set维护线段就可以解决了. 容易想到 第二个操作借用启发式合并可以得到一个很不错的复杂度 不过利用线段树维护这个东西 在区间 ...

  4. win系统下git代码批量克隆,批量更新

    @REM 根据实际情况设置GIT路径及本地仓库地址 set path=%path%;"D:\Program Files\Git\cmd" set project_path=F:\g ...

  5. day2. 六大基本数据类型简介

    一.基本数据类型 Number 数字类型 (int float bool complex) str 字符串类型 list 列表类型 tuple 元组类型 set 集合类型 dict 字典类型 二.Nu ...

  6. Linux常用命令之cp、mv、rm、cat、more、head、tail、ln命令讲解

    上一章节中,我们了解到了Linux系统的最基础的几个文件处理命令,核心的是ls命令,在今天这章中,我们来继续学习Linux对于文件操作相关的一些命令,比如复制.移动.删除.查看等命令. 1.cp 命令 ...

  7. 当面试官问我ArrayList和LinkedList哪个更占空间时,我这么答让他眼前一亮

    前言 今天介绍一下Java的两个集合类,ArrayList和LinkedList,这两个集合的知识点几乎可以说面试必问的. 对于这两个集合类,相信大家都不陌生,ArrayList可以说是日常开发中用的 ...

  8. CI4框架应用五 - 加载视图

    这节我们来看一下CI4框架中视图的加载, CI4中提供了几种方式加载视图. 1. 利用CI4框架提供的全局函数view(‘模板名’),如果需要传参数可以在第二个参数传一个数组 我们先修改一下之前定义的 ...

  9. 2019.12.9Java课堂总结

    今天在课堂上进行了练习.现进行成果及不足汇报: 1.完成了登录界面的设计 2.完成了数据库的连接. 3.完成了数据库表的设计   4.完成了变量的定义与初始化以及get.set的设立. 5.对整体框架 ...

  10. Layui+MVC+EF (项目从新创建开始)

    最近学习Layui ,就准备通过Layui来实现之前练习的项目, 先创建一个新的Web 空项目,选MVC 新建项目 创建各种类库,模块之间添加引用,并安装必要Nuget包(EF包)   模块名称 模块 ...