Meissel–Lehmer 算法
前言
推荐先行阅读我的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 算法的更多相关文章
- Meissel Lehmer Algorithm 求前n个数中素数个数 【模板】
Count primes Time Limit: 12000/6000 MS (Java/Others) Memory Limit: 65536/65536 K (Java/Others)Tot ...
- 求第 i 个素数 Meissel Lehmer Algorithm + 二分 【模板】
1473: L先生与质数V3 Time Limit: 1 Sec Memory Limit: 128 MBSubmit: 1348 Solved: 147 [Submit][Status][Web ...
- 【算法学习笔记】Meissel-Lehmer 算法 (亚线性时间找出素数个数)
「Meissel-Lehmer 算法」是一种能在亚线性时间复杂度内求出 \(1\sim n\) 内质数个数的一种算法. 在看素数相关论文时发现了这个算法,论文链接:Here. 算法的细节来自 OI w ...
- 洛谷 P3912 素数个数
P3912 素数个数 题目描述 求1,2,\cdots,N1,2,⋯,N 中素数的个数. 输入输出格式 输入格式: 1 个整数NN. 输出格式: 1 个整数,表示素数的个数. 输入输出样例 输入样例# ...
- [LOJ6235]区间素数个数
题目大意: 给定$n(n\leq10^{11})$,求$\pi(n)$. 思路: 计算$\pi$函数有$O(n^{\frac23})$的Lehmer算法,这里考虑$O(\frac{n^{\frac34 ...
- B树——算法导论(25)
B树 1. 简介 在之前我们学习了红黑树,今天再学习一种树--B树.它与红黑树有许多类似的地方,比如都是平衡搜索树,但它们在功能和结构上却有较大的差别. 从功能上看,B树是为磁盘或其他存储设备设计的, ...
- 分布式系列文章——Paxos算法原理与推导
Paxos算法在分布式领域具有非常重要的地位.但是Paxos算法有两个比较明显的缺点:1.难以理解 2.工程实现更难. 网上有很多讲解Paxos算法的文章,但是质量参差不齐.看了很多关于Paxos的资 ...
- 【Machine Learning】KNN算法虹膜图片识别
K-近邻算法虹膜图片识别实战 作者:白宁超 2017年1月3日18:26:33 摘要:随着机器学习和深度学习的热潮,各种图书层出不穷.然而多数是基础理论知识介绍,缺乏实现的深入理解.本系列文章是作者结 ...
- 红黑树——算法导论(15)
1. 什么是红黑树 (1) 简介 上一篇我们介绍了基本动态集合操作时间复杂度均为O(h)的二叉搜索树.但遗憾的是,只有当二叉搜索树高度较低时,这些集合操作才会较快:即当树的高度较高(甚至一种极 ...
- 散列表(hash table)——算法导论(13)
1. 引言 许多应用都需要动态集合结构,它至少需要支持Insert,search和delete字典操作.散列表(hash table)是实现字典操作的一种有效的数据结构. 2. 直接寻址表 在介绍散列 ...
随机推荐
- 【pandas小技巧】--反转行列顺序
反转pandas DataFrame的行列顺序是一种非常实用的操作.在实际应用中,当我们需要对数据进行排列或者排序时,通常会使用到Pandas的行列反转功能.这个过程可以帮助我们更好地理解数据集,发现 ...
- 使用 VirtualBox+Vagrant 创建 CentOS7 虚拟机
一.准备工作 1.1 软件下载 VirtualBox:Downloads – Oracle VM VirtualBox Vagrant:Install | Vagrant | HashiCorp De ...
- pc 移动端 双端切换
实现一个项目匹配多个端,使用vue.config自带的page 实现多个页面切换.官网介绍:https://cli.vuejs.org/zh/config/#pages 在创建的vue项目中找到 vu ...
- [oracle]拆分多用户的公共表空间
前言 开发环境之前多个用户共用一个表空间,后期维护比较麻烦,因此需要将这些用户拆出来,一个用户一个表空间,以后清理这些用户也更方便. 大致思路:假设A.B.C用户共用一个表空间,将A.B.C的用户数据 ...
- ATtiny88初体验(二):呼吸灯
ATtiny88初体验(二):呼吸灯 前面的"点灯"实验实现了间隔点亮/熄灭LED,但是间隔时间和亮度都没法控制,为了解决这个问题,可以使用ATtiny88的定时器模块. ATti ...
- CodeForces 1367D Task On The Board
题意 给一个字符串\(t\),和一个长度为\(m\)的数组\(b[]\),要求构造一个字符串\(s\),\(s\)中的字符都出现在\(t\)中,对于\(s[i]\)而言,对于任意\(j\),如果有\( ...
- 使用 PDF一机一码加密大师,加密打包PDF文件(一机一码,绑定机器,无需额外安装阅读器)
PDF一机一码加密大师, 可以加密任意PDF文档,添加一机一码授权, 静态密码等, 可以禁止用户复制,打印PDF文档中的内容,并且加密生成的PDF在其他用户电脑上无需安装第三方阅读器即可直接阅读. 下 ...
- 【NET 7.0、OpenGL ES】使用Silk.NET渲染MMD,并实时进行物理模拟。
有关mmd播放器,网上也有许多非常漂亮的实现,如 pmxeditor.saba.blender_mmd_tools等等.. 首先我想先介绍下我参考实现的仓库: sselecirPyM/Coocoo3D ...
- 从内核世界透视 mmap 内存映射的本质(原理篇)
本文基于内核 5.4 版本源码讨论 之前有不少读者给笔者留言,希望笔者写一篇文章介绍下 mmap 内存映射相关的知识体系,之所以迟迟没有动笔,是因为 mmap 这个系统调用看上去简单,实际上并不简单, ...
- Solution -「CSP 2019」Centroid
Description Link. 给定一棵 \(n\) 个点的树,设 \(E\) 为边集,\(V'_x,\ V'_y\) 分别为删去边 \((x,y)\) 后 点 \(x\) 所在的树的点集和点 \ ...