前言

推荐先行阅读我的blog文章————Min_25 筛

什么是Meissel–Lehmer 算法

Meissel-Lehmer 算法是一种基于 \(ϕ\) 函数的的快速计算前缀质数个数(当然也可以推广到前缀和质数幂次)的算法。

【模板例题】Meissel–Lehmer 算法

给定整数 \(n\),求出 \(\pi(n)\) 的值。

\(\pi(n)\) 表示 \(1 \sim n\) 的整数中质数的个数。

对于 \(100\%\) 的数据,\(1 \leq n \leq 10^{13}\)。

解法

前置知识:Eratosthenes 筛法。Eratosthenes 筛法是最简单的筛法之一

int Eratosthenes(int n) {
int p = 0;
for (int i = 0; i <= n; ++i) is_prime[i] = 1;
is_prime[0] = is_prime[1] = 0;
for (int i = 2; i <= n; ++i) {
if (is_prime[i]) {
prime[p++] = i;
if ((long long)i * i <= n) for (int j = i * i; j <= n; j += i)
is_prime[j] = 0; //(*)
}
}
return p;
}

我们只需要知道在标星号的那一行代码执行时有多少个 is_prime 从 true 变为 false 。如果我们可以维护这个值,我们就可以知道从 1 到 n 的素数个数了。据说,这个东西在 Project Euler 上曾经有一个所谓 Lucy DP 的东西可以解决,但是这个不是现在我们想要的。

定义: 记 is_prime 为 \(a\),\(S(v,p)\) 为 \(a_j ( 2\le j\le v)\)在 \(i=p\)时处理完毕后为 true 的个数。

然后上标处理,虽然时空复杂度达不到线性,但很快了。

之后修为Min_25筛发

在 min_25 的代码中的一些变量解释:

\(\texttt{smalls}[i] = S(i, p)\)

\(\texttt{roughs}[i]\):没有被筛掉的第 \(i\) 个整数。

其大小由一个变量 \(\texttt s\) 维护。

把 \(1\) 留下,过筛时使用的素数会消失。

\(\texttt{larges}[i] = S(\lfloor n/\texttt{roughs}[i]\rfloor, p)\)

\(\texttt{pc} = \pi(p-1)\)

\(\texttt{skip}[i]\):\(i\) 如果被筛出来的话就是真的。

但是不更新偶数。

using i64 = long long;
int isqrt(i64 n) {
return sqrtl(n);
}
__attribute__((target("avx"), optimize("O3", "unroll-loops")))
i64 prime_pi(const i64 N) {
if (N <= 1) return 0;
if (N == 2) return 1;
const int v = isqrt(N);
int s = (v + 1) / 2;
vector<int> smalls(s); for (int i = 1; i < s; ++i) smalls[i] = i;
vector<int> roughs(s); for (int i = 0; i < s; ++i) roughs[i] = 2 * i + 1;
vector<i64> larges(s); for (int i = 0; i < s; ++i) larges[i] = (N / (2 * i + 1) - 1) / 2;
vector<bool> skip(v + 1);
const auto divide = [] (i64 n, i64 d) -> int { return double(n) / d; };
const auto half = [] (int n) -> int { return (n - 1) >> 1; };
int pc = 0;
for (int p = 3; p <= v; p += 2) if (!skip[p]) {
int q = p * p;
if (i64(q) * q > N) break;
skip[p] = true;
for (int i = q; i <= v; i += 2 * p) skip[i] = true;
int ns = 0;
for (int k = 0; k < s; ++k) {
int i = roughs[k];
if (skip[i]) continue;
i64 d = i64(i) * p;
larges[ns] = larges[k] - (d <= v ? larges[smalls[d >> 1] - pc] : smalls[half(divide(N, d))]) + pc;
roughs[ns++] = i;
}
s = ns;
for (int i = half(v), j = ((v / p) - 1) | 1; j >= p; j -= 2) {
int c = smalls[j >> 1] - pc;
for (int e = (j * p) >> 1; i >= e; --i) smalls[i] -= c;
}
++pc;
}
larges[0] += i64(s + 2 * (pc - 1)) * (s - 1) / 2;
for (int k = 1; k < s; ++k) larges[0] -= larges[k];
for (int l = 1; l < s; ++l) {
int q = roughs[l];
i64 M = N / q;
int e = smalls[half(M / q)] - pc;
if (e < l + 1) break;
i64 t = 0;
for (int k = l + 1; k <= e; ++k) t += smalls[half(divide(M, roughs[k]))];
larges[0] += t - i64(e - l) * (pc + l - 1);
}
return larges[0] + 1;
}

仔细看上面的代码应该能大致了解算法,要是不懂可以看这里我的一些解释:

roughs 的更新:roughs[..s]是未过筛的整数。ns 是下一个循环中的 s。ns = 0,在 roughs[ns] 中有值的话 ns 就自增。

larges[smalls[d]-pc] 的含义:smalls[d] - pc 是 “未过筛剩下的整数中 d 是第几个数字?” 的意思。

smalls[i] 的更新:保持 \(j = \frac i p\)被更新。当 \(i \ge pj\) 时,smalls[i] 被更新,它的值被减去 smalls[j] - pc,也就是被减去 smalls[i / p] - pc。

完整代码

#include <stdio.h>
#include <cmath>
#include <algorithm>
#include <vector>
#define qaq inline
typedef long long ll;
qaq int isqrt(ll n){
return std::sqrt(n);
}
qaq ll half(ll n){
return (n-1)>>1;
}
qaq ll divide(ll n,ll base){
return double(n)/base;
}
ll piSieve(const ll n){
if(n<=1) return 0LL;
if(n==2) return 1LL;
const int lim=isqrt(n);
int vsz=(lim+1)>>1;
std::vector<int> smalls(vsz);
for(int cx=0;cx<vsz;++cx) smalls[cx]=cx;
std::vector<int> roughs(vsz);
for(int cx=0;cx<vsz;++cx) roughs[cx]=(cx<<1|1);
std::vector<ll> larges(vsz);
for(int cx=0;cx<vsz;++cx) larges[cx]=(n/(cx<<1|1)-1)>>1;
std::vector<bool> skips(lim+1);
int pCnt=0;
for(int p=3;p<=lim;p+=2){
if(skips[p]) continue;
int p2=p*p;
if(1LL*p2*p2>n) break;
skips[p]=true;
for(int cx=p2;cx<=lim;cx+=(p<<1))
skips[cx]=true;
int ns=0;
for(int cx=0;cx<vsz;++cx){
int cur=roughs[cx];
if(skips[cur]) continue;
ll d=1LL*cur*p;
larges[ns]=larges[cx]-(d<=lim?larges[smalls[d>>1]-pCnt]
:smalls[half(divide(n,d))])+pCnt;
roughs[ns++]=cur;
}
vsz=ns;
for(int cx=half(lim),cy=((lim/p)-1)|1;cy>=p;cy-=2){
int cur=smalls[cy>>1]-pCnt;
for(int cz=(cy*p)>>1;cz<=cx;--cx)
smalls[cx]-=cur;
}
++pCnt;
}
larges[0]+=1LL*(vsz+((pCnt-1)<<1))*(vsz-1)>>1;
for(int cx=1;cx<vsz;++cx) larges[0]-=larges[cx];
for(int cx=1;cx<vsz;++cx){
int q=roughs[cx];
ll m=n/q;
int e=smalls[half(m/q)]-pCnt;
if(e<cx+1) break;
ll t=0;
for(int cy=cx+1;cy<=e;++cy)
t+=smalls[half(divide(m,roughs[cy]))];
larges[0]+=t-1LL*(e-cx)*(pCnt+cx-1);
}
return larges[0]+1;
}
int main(){
ll n;
scanf("%lld",&n);
printf("%lld\n",piSieve(n));
return 0;
}

Meissel–Lehmer 算法的更多相关文章

  1. Meissel Lehmer Algorithm 求前n个数中素数个数 【模板】

    Count primes Time Limit: 12000/6000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others)Tot ...

  2. 求第 i 个素数 Meissel Lehmer Algorithm + 二分 【模板】

    1473: L先生与质数V3 Time Limit: 1 Sec  Memory Limit: 128 MBSubmit: 1348  Solved: 147 [Submit][Status][Web ...

  3. 【算法学习笔记】Meissel-Lehmer 算法 (亚线性时间找出素数个数)

    「Meissel-Lehmer 算法」是一种能在亚线性时间复杂度内求出 \(1\sim n\) 内质数个数的一种算法. 在看素数相关论文时发现了这个算法,论文链接:Here. 算法的细节来自 OI w ...

  4. 洛谷 P3912 素数个数

    P3912 素数个数 题目描述 求1,2,\cdots,N1,2,⋯,N 中素数的个数. 输入输出格式 输入格式: 1 个整数NN. 输出格式: 1 个整数,表示素数的个数. 输入输出样例 输入样例# ...

  5. [LOJ6235]区间素数个数

    题目大意: 给定$n(n\leq10^{11})$,求$\pi(n)$. 思路: 计算$\pi$函数有$O(n^{\frac23})$的Lehmer算法,这里考虑$O(\frac{n^{\frac34 ...

  6. B树——算法导论(25)

    B树 1. 简介 在之前我们学习了红黑树,今天再学习一种树--B树.它与红黑树有许多类似的地方,比如都是平衡搜索树,但它们在功能和结构上却有较大的差别. 从功能上看,B树是为磁盘或其他存储设备设计的, ...

  7. 分布式系列文章——Paxos算法原理与推导

    Paxos算法在分布式领域具有非常重要的地位.但是Paxos算法有两个比较明显的缺点:1.难以理解 2.工程实现更难. 网上有很多讲解Paxos算法的文章,但是质量参差不齐.看了很多关于Paxos的资 ...

  8. 【Machine Learning】KNN算法虹膜图片识别

    K-近邻算法虹膜图片识别实战 作者:白宁超 2017年1月3日18:26:33 摘要:随着机器学习和深度学习的热潮,各种图书层出不穷.然而多数是基础理论知识介绍,缺乏实现的深入理解.本系列文章是作者结 ...

  9. 红黑树——算法导论(15)

    1. 什么是红黑树 (1) 简介     上一篇我们介绍了基本动态集合操作时间复杂度均为O(h)的二叉搜索树.但遗憾的是,只有当二叉搜索树高度较低时,这些集合操作才会较快:即当树的高度较高(甚至一种极 ...

  10. 散列表(hash table)——算法导论(13)

    1. 引言 许多应用都需要动态集合结构,它至少需要支持Insert,search和delete字典操作.散列表(hash table)是实现字典操作的一种有效的数据结构. 2. 直接寻址表 在介绍散列 ...

随机推荐

  1. 王道oj/problem12(动态申请内存存储数组)

    网址:http://oj.lgwenda.com/problem/12 思路:用输入的整型创建对应数组,用scanf消除换行键: 用gets()输入语句并输出,再释放. 代码: #define _CR ...

  2. Unity的IPreprocessShaders:深入解析与实用案例

    Unity IPreprocessShaders Unity IPreprocessShaders是Unity引擎中的一个非常有用的功能,它可以让开发者在编译Shader时自定义哪些操作需要被执行.这 ...

  3. 部署ELK+filebeat收集nginx日志

    前言 简介 ELK(Elasticsearch.Logstash.Kibana)是开源的实时日志收集分析解决方案. Elasticsearch:开源搜索引擎,是一个基于Lucene.分布式.通过Res ...

  4. Windows校验文件MD5和SHA值的方法

    1.需求背景 下载或传输文件后,需要计算文件的MD5.SHA256等校验值,以确保下载或传输后的文件和源文件一致 2.校验方法 如上图所示,可以使用Windows自带的certutil命令来计算一个文 ...

  5. Laf & 中大猫谱:让每一只流浪猫都有家

    猫谱简介 中大猫谱是一款辅助校园流浪猫救助的开源小程序项目,服务端使用 Laf 云开发. 猫谱主要功能包括:猫咪信息登记.照片分享.拍照识猫.公告和留言等.项目创立的初衷,是解决校园猫猫交流群里的一个 ...

  6. 一篇关于获得拼多多商品详情 API的使用说明

    拼多多(Pinduoduo)是中国一家快速发展的电商平台,为了帮助开发者更好地接入拼多多,平台提供了丰富的 API 接口供开发者使用,其中包括获取拼多多商品详情的 API.接下来,我们将介绍如何使用拼 ...

  7. jenkins部署及gitlab联调

    jenkins部署及gitlab联调 目录 jenkins部署及gitlab联调 一.jenkins安装 1.环境优化 2.安装jdk java 环境 3.下载jenkins 4.启动Jenkins服 ...

  8. 从Google开发者大会浅谈LLM的应用

    这周参加了在上海世博中心举办Google I/O Connect中国开发者大会,有几年没参加这么高质量的活动,有点感慨. 期间重点听了关于GCP和Google AI大语言模型的主题演讲,发现目前各大厂 ...

  9. BZ全景编辑器(KRPano全景可视化编辑器, 无需编写代码, 图形化制作全景漫游)

    软件简介 BZ全景编辑器是一款KRPano全景可视化编辑工具,下载安装即可使用,无需拥有任何KRPano代码基础,便可以制作生成精美的全景漫游作品. BZ全景编辑器群:882083973 最新版软件下 ...

  10. java中有哪些并发的List?只知道一种的就太逊了

    java中有很多list,但是原生支持并发的并不多,我们在多线程的环境中如果想同时操作同一个list的时候,就涉及到了一个并发的过程,这时候我们就需要选择自带有并发属性的list,那么java中的并发 ...