【问题标题】:Looking for a fast prime-counting function寻找快速素数计数功能
【发布时间】:2017-04-16 01:16:24
【问题描述】:

我需要计算小于或等于某个N的素数个数,即素数计数函数或PI函数。我有这个,但运行速度太慢:

function PI(x) {
    var primes = 4;
    for (var i = 3; i <= x; i += 2) {
        if (i % 3 === 0 || i % 5 === 0 || i % 7 === 0) continue;
        var r = ~~Math.sqrt(i), p = true;
        for (var j = 2; j <= r; j++) {
            if (i % j === 0) {
                p = false;
                break;
            }
        }
        if (p)
            primes++;
    }
    return primes;
}

计算PI(1000000) 需要将近一秒钟,而计算PI(10000000) 需要长达20 秒。那太慢了。有更快的方法吗?

【问题讨论】:

  • 埃拉托色尼筛算法是一种相当有效的方法来查找直到 n 的所有素数。 en.wikipedia.org/wiki/Sieve_of_Eratosthenes
  • 问题代码这么慢的原因是它是一个非常低效的试除法版本; Eratosthenes such as my answer in another thread 的页分段位压缩筛的正确实现将在一秒内找到十亿的素数,在一分钟内找到一千亿(1e11),但这仍然不是真正的现代素数计数功能。我下面的答案可以在几分之一秒内完成这些,并且可以在一分钟左右将素数数到2**53 - 1(大约 9e15)。

标签: javascript


【解决方案1】:

我迟到了,但被问题标题中的“素数计数功能”触发了,因为这并没有引起太多关注。

TLDR;改性活生物体如何运作?跳到最后查看代码和结果...

素数计数函数的主要进步历史如下:

  1. 大约在 1830 年,Legendre 开发了一个公式,该公式使用“包含-排除原理”来计算低于x 的素数的数量,而无需枚举所有单个素数,并且只需要知道基本素数的平方根x
  2. 在 1870 年,E. D. F. Meissel 改进了勒让德的公式,在一定程度上减少了运算次数,但需要知道素数达到 x^(2/3) 而不是 x^(1/2),其中“知道素数”通常由 SoE 执行,但只需要存储O(x^(1/3)/(ln x)) 而不是O(x^(1/2)/(ln x)),尽管它只会将时间复杂度从O(x^(2/3)/((ln x)^2)) 降低到O(x^(2/3)/((ln x)^3))
  3. 最后Lagarias, Miller, and Odzlinko (LMO) developed their algorithm in 1985改进了Meissel方法,基本版本仍然需要与x^(2/3)相同的“素值知识”,但通过将计算分成两部分大大减少了计算“Phi”所需的时间一组计算(至少,更多用于以后的改进),一个使用递归整数除法通过通常的 Meissel 方法确定“Phi”函数的“普通”叶,但这项工作可以忽略不计,因为只有大约 x^(1/3) 的这些,以及通过使用部分筛分和计数技术来计算剩余的“特殊”叶子的时间大大减少。可以使用部分筛分,因为“Phi(x, a)”的定义是所有低于x 且不能被低于ath 素数的素数整除的数的计数,这正是筛分产生的结果高达x 的基本素数的倍数,直到ath 素数。基本 LMO 方法的计算复杂度为O(x^(2/3)(log (log x)))

为了了解这些技术是如何使用的,让我们在将素数计数到 64 的平凡范围时依次考虑它们,如下所示:

  1. 考虑到 64 的素数,我们可以很容易地确定为 2、3、5、7、11、13、17、19、23、29、31、37、41、43、47、53、57、和 61,总共 18 个素数;对于这个练习,考虑到对于勒让德技术(2、3、5 和 7 的四个素数)或基于 Meissel 的技术,我们只知道 64 的平方根的值是 8 到 64 的范围^(2/3) 是 16(2、3、5、7、11 和 13 的六个素数)。
  2. Legendre 的技术说我们只需要计算 "Phi(64, 6) + 6 - 1" 的值,其中 6 是平方根以下的质数; “Phi”函数是在该限制之前的未排除值的数量,即 64,因为它们不是小于限制平方根的六个基本素数的倍数,因此被包括在内,并且通过递归技术计算如上一个答案中使用的那样,在这种情况下表示为64 - 64/2 - 64/3 + 64/2/3 - 64/5 + 64/5/2 + 64/5/3 - 64/5/2/3 - 64/7 + 64/7/2 + 64/7/3 - 64/7/2/3 + 64/7/5 - 64/7/5/2 - 64/7/5/3 + 64/7/5/2/3 - 64/11 + 64/11/2 + 64/11/3 - 64/11/2/3 + ... + 64/13 - 64/13/2 - 64/13/3 ... = 13,当加六减一时,正确答案为18。这被称为“包含-排除原则”,因为素数为奇数的项被排除/减去,并且具有偶数个素因数的项被包括/添加。您可以看到这需要大量的除法运算,尽管在上一个答案中使用了诸如缓存之类的技术来减少如此小的范围内的这些,但不幸的是只能达到极限。
  3. 对于 Meissel 技术,计算 Phi(64, 2) + 2 - 1 其中 2 是直到四的立方根的质数更容易计算,因为 64 - 64/2 - 64/3 + 64/2/3 = 21 加上 2 和 1 减去是 22 但这不是最终答案,因为我们需要使用 4 范围的立方根和 8 范围的平方根之间的质数减去“P2”项,即“双打”的数量,因此基于素数 5 和 7,并计算为素数数之和,最高为 64/5 - 3 + 1 和 64/7 - 4 + 1,即 5 - 3 + 1 和 4 - 4 + 1 是 3 的和,1 等于 4。这些代表 5 乘 7、5 乘 9 和 5 乘 11 的乘积,以及 7 乘 9 的乘积,即补偿的四对。所以最终的答案是 22 减去这 4 对是 18 应该是。
  4. LMO 技术的工作原理与上述完全相同,只是对于这个微不足道的限制有一个“特殊”叶,即 64/2/3 叶,因为 2 乘以 3 超过了 64 的立方根,即 4。如果实施这就像 LMO 会做的那样,而不是做这个除法,我们运行 SoE 直到 2 和 3 的基本素数都被筛选出它们的所有倍数,然后将剩余的值数计数到商以获得相同的答案10 为叶子。对于这样一个微不足道的限制,“特殊”叶子的数量很少,但重要的是特殊叶子的数量只会随着O((limit^(1/3))/(log (limit^(1/3)))^2) 而增加。

那么我们如何实现基本的 LMO 算法呢?有以下几个步骤:

  1. 我们制作了一个质数表,直到极限的立方根(大小约为(limit^(1/3)) / (ln limit)),这需要的时间可以忽略不计。
  2. 我们可以使用这些素数通过递归除法技术计算“Phi”的“S1”普通部分,这需要的时间可以忽略不计,因为计算量很少,只有O(limit^(1/3))
  3. 我们从基本素数表(最大的表为O(limit^(1/3)) 的大小)制作了一个素数计数值的表,直到立方根;我们还为每个部分筛选的步骤制作了一个中间“Phi”计数表,直到立方根基素值,以及一个确定的“S2”根叶参数表,这些参数实际上可以在“S1”时确定上面正在计算值;所有这些花费的时间都可以忽略不计,并且最大的(例如最后一个)仅与范围的立方根成正比,这就是 O(limit^(1/3)) 中使用空间的原因。
  4. 我们按页面段筛选直到limit^(2/3) 范围,其中每个页面在根“S2”叶表中找到的最小值以上的时间被一个基本素值部分剔除,累积计数的总和直到每个“S2”叶商值的未剔除值的数量。
  5. 基本的页面分段仅赔率位压缩 SoE 需要大约 80 秒来计算素数,直至达到 53 位限制的三分之二幂,但我们需要使用修改后的版本来更新计数器表一些剔除操作的每个新剔除值,因此它需要大约一半的时间或大约 120 秒。
  6. 在此实现中,直到每个“S2”点的未剔除值的计数使用批处理计数器数组,增量的最终范围很小,小于通过快速“弹出计数”技术计算的批处理大小。最初的 LMO 论文描述了使用二叉索引树来完成此任务,但并未尝试减少树深度,这也是它比此实现稍慢的部分原因,因为它在剔除时间中添加了 log x 因子这个log x在剔除时更新计数器的成本因素会使剔除时间慢很多倍;对于此实现的上述最大范围,“特殊”叶子数量的计数和累积只需要几十秒的时间。解释了此成本并提出了解决方案by Kim Walisch
  7. 当对于给定页段的“S2”计算所需的每个单独限制的所有基本素数都已完成时,然后将页面段完全剔除为小于平方的基本素数段中表示的最大值和段范围内的“P2”计数值是从这个完成的筛选页面确定的,无需再次筛选相同的范围。
  8. 当所有达到range^(2/3) 限制的页面段完成后,累加器将包含“S2”和“P2”所需的值,以便S1 + S2 - P2 + number of base primes to the cube root of the limit - 1 可以作为素数计数的所需答案输出到那个limit

以下代码实现了基本的 LMO 算法(没有“alpha”优化,这会大大增加代码复杂性),“S2”特殊叶子的代码松散但不完全翻译from the "primecount" C++ code by Kim Walisch:

"use strict";

const MAXVALUE = 9007199254740991; // 2**53 - 1

// creates a function returning a lazily memoized value from a thunk...
function lazy(thunk) {
  let value = undefined;
  return function() {
if (value === undefined) { value = thunk(); thunk = null; }
return value;
  }
}

// a page-segmented odds-only bit-packed Sieve of Eratosthenes;

const PGSZBITS = 262144; // about CPU l1 cache size in bits (power of two)

const CLUT = function () { // fast "pop count" Counting Look Up Table...
  const arr = new Uint8Array(65536);
  for (let i = 0; i < 65536; ++i) {
let nmbts = 0 | 0; let v = i;
while (v > 0) { ++nmbts; v &= (v - 1) | 0; }
arr[i] = nmbts | 0; }
  return arr;
}();

function countPageFromTo(bitstrt, bitlmt, sb) {
  const fst = bitstrt >> 5; const lst = bitlmt >> 5;
  const pg = new Uint32Array(sb.buffer);
  let v0 = (pg[fst] | ((0xFFFFFFFF >>> 0) << (bitstrt & 31))) >>> 0;
  let cnt = ((lst - fst) << 5) + CLUT[v0 & 0xFFFF]; cnt += CLUT[v0 >>> 16];
  for (let i = fst | 0; i < lst; ++i) {
let v = pg[i] >>> 0;
cnt -= CLUT[v & 0xFFFF]; cnt -= CLUT[v >>> 16];
  }
  let v1 = (pg[lst] | ((0xFFFFFFFE >>> 0) << (bitlmt & 31))) >>> 0;
  cnt -= CLUT[v1 & 0xFFFF]; cnt -= CLUT[v1 >>> 16]; return cnt | 0;
}

function partialSievePage(lwi, bp, sb) {
  const btsz = sb.length << 3;
  let s = Math.trunc((bp * bp - 3) / 2); // compute the start index...
  if (s >= lwi) s -= lwi; // adjust start index based on page lower limit...   
  else { // for the case where this isn't the first prime squared instance
let r = ((lwi - s) % bp) >>> 0;
s = (r != (0 >>> 0) ? bp - r : 0) >>> 0; }
  if (bp <= 32) {
for (let slmt = Math.min(btsz, s + (bp << 3)); s < slmt; s += bp) {
  const shft = s & 7; const msk = ((1 >>> 0) << shft) >>> 0;
  for (let c = s >> 3, clmt = sb.length; c < clmt | 0; c += bp)
    sb[c] |= msk; } }
  else
for (let slmt = sb.length << 3; s < slmt; s += bp)
  sb[s >> 3] |= ((1 >>> 0) << (s & 7)) >>> 0;
}

function partialSieveCountPage(lwi, bp, cntarr, sb) {
  const btsz = sb.length << 3; let cullcnt = 0;
  let s = Math.trunc((bp * bp - 3) / 2); // compute the start index...
  if (s >= lwi) // adjust start index based on page lower limit...
s -= lwi;
  else { // for the case where this isn't the first prime squared instance
let r = ((lwi - s) % bp) >>> 0;
s = (r != (0 >>> 0) ? bp - r : 0) >>> 0; }
  if (bp <= 32) {
for (let slmt = Math.min(btsz, s + (bp << 3)); s < slmt; s += bp) {
  const shft = s & 7; const msk = ((1 >>> 0) << shft) >>> 0;
  for (let c = s >>> 3, clmt = sb.length; c < clmt | 0; c += bp) {
    const isbit = ((sb[c] >>> shft) ^ 1) & 1;
    cntarr[c >> 6] -= isbit; cullcnt += isbit; sb[c] |= msk; }
}
  }
  else
for (let slmt = sb.length << 3; s < slmt; s += bp) {
  const sba = s >>> 3; const shft = s & 7;
  const isbit = ((sb[sba] >>> shft) ^ 1) & 1;
  cntarr[s >> 9] -= isbit; cullcnt += isbit;
  sb[sba] |= ((1 >>> 0) << shft) >>> 0; }
  return cullcnt;
}

// pre-culled pattern of small wheel primes...
const WHLPRMS = [ 2, 3, 5, 7, 11, 13, 17 ];
const WHLPTRNLEN = WHLPRMS.reduce((s, v) => s * v, 1) >>> 1; // odds only!
const WHLPTRN = function() { // larger than WHLPTRN by one buffer for overflow
  const len = (WHLPTRNLEN + (PGSZBITS >>> 3) + 3) & (-4); // up 2 even 32 bits!
  const arr = new Uint8Array(len);
  for (let bp of WHLPRMS.slice(1)) partialSievePage(0, bp, arr);
  arr[0] |= ~(-2 << ((WHLPRMS[WHLPRMS.length - 1] - 3) >> 1)) >>> 0; return arr;
}();

function fillPage(lwi, sb) {
  const mod = (lwi / 8) % WHLPTRNLEN;
  sb.set(new Uint8Array(WHLPTRN.buffer, mod, sb.length));
}

function cullPage(lwi, bpras, sb) {
  const btsz = sb.length << 3; let bp = 3;
  const nxti = lwi + btsz; // just beyond the current page 
  for (let bpra of bpras()) {
for (let bpri = 0; bpri < bpra.length; ++bpri) {
  const bpr = bpra[bpri]; bp += bpr + bpr;
  let s = (bp * bp - 3) / 2; // compute start index of prime squared
  if (s >= nxti) return; // enough bp's
  partialSievePage(lwi, bp, sb);
}
  }
}

function soePages(bitsz, bpras) {
  const buf =  new Uint8Array(bitsz >> 3); let lowi = 0;
  const gen = bpras === undefined ? makeBasePrimeRepArrs() : bpras;
  return function*() {
while (true) {
  fillPage(lowi, buf); cullPage(lowi, gen, buf);
  yield { lwi: lowi, sb: buf }; lowi += bitsz; }
  };
}

function makeBasePrimeRepArrs() {
  const buf = new Uint8Array(128); let gen = undefined; // avoid data race!
  fillPage(0, buf);
  for (let i = 8, bp = 19, sqr = bp * bp; sqr < 2048+3;
                                      ++i, bp += 2, sqr = bp * bp)
if (((buf[i >> 3] >>> 0) & ((1 << (i & 7)) >>> 0)) === 0 >>> 0)
  for (let c = (sqr - 3) >> 1; c < 1024; c += bp)
    buf[c >> 3] |= (1 << (c & 7)) >>> 0; // init zeroth buf
  function sb2bprs(sb) {
const btsz = sb.length << 3; let oi = 0;
const arr = new Uint8Array(countPageFromTo(0, btsz - 1, sb));
for (let i = 0, j = 0; i < btsz; ++i)
  if (((sb[i >> 3] >>> 0) & ((1 << (i & 7)) >>> 0)) === 0 >>> 0) {
    arr[j++] = (i - oi) >>> 0; oi = i; }
return { bpra: arr, lastgap: (btsz - oi) | 0 };
  }
  let { bpra, lastgap } = sb2bprs(buf);
  function next() {
const nxtpg = sb2bprs(gen.next().value.sb);
nxtpg.bpra[0] += lastgap; lastgap = nxtpg.lastgap;
return { head: nxtpg.bpra, tail: lazy(next) };
  }
  const lazylist = { head: bpra, tail: lazy(function() {
if (gen === undefined) {
  gen = soePages(1024)(); gen.next() } // past first page
return next();
  }) };
  return function*() { // return a generator of rep pages...
let ll = lazylist; while (true) {  yield ll.head; ll = ll.tail(); }
  };
}

function *revPrimesFrom(top, bpras) {
  const topndx = (top - 3) >>> 1;
  const buf = new Uint8Array(PGSZBITS >>> 3);
  let lwi = (((topndx / PGSZBITS) >>> 0) * PGSZBITS) >>> 0;
  let si = (topndx - lwi) >>> 0;
  for (; lwi >= 0; lwi -= PGSZBITS) { // usually external limit!
const base = 3 + lwi + lwi;
fillPage(lwi, buf); cullPage(lwi, bpras, buf);
for (; si >= 0 >>> 0; --si)
  if (((buf[si >> 3] >>> 0) & ((1 << (si & 7)) >>> 0)) === (0 >>> 0))
    yield base + si + si;
si = PGSZBITS - 1;
  }
};

const TinyPrimes = [ 2, 3, 5, 7, 11, 13, 17, 19 ]; // degree eight
const TinyProduct = TinyPrimes.reduce((s, v) => s * v) >>> 0;
const TinyHalfProduct = TinyProduct >>> 1;
const TinyTotient = TinyPrimes.reduce((s, v) => s * (v - 1), 1) >>> 0;
const TinyLength = (TinyProduct + 8) >>> 2; // include zero and half point!
const TinyTotients = function() {
  const arr = new Uint32Array(TinyLength | 0);
  arr[TinyLength - 1] = 1; // mark mid point value as not prime - never is
  let spn = 3 * 5 * 7; arr[0] = 1; // mark zeroth value as not prime!
  for (let bp of [ 3, 5, 7 ]) // cull small base prime values...
for (let c = (bp + 1) >>> 1; c <= spn; c += bp) arr[c] |= 1;
  for (let bp of [ 11, 13, 17, 19 ]) {
for (let i = 1 + spn; i < TinyLength; i += spn) {
  const rng = i + spn > TinyLength ? spn >> 1 : spn;
  arr.set(new  Uint32Array(arr.buffer, 4, rng), i); }
spn *= bp;
for (let c = (bp + 1) >>> 1; // eliminate prime in pattern!
       c < (spn > TinyLength ? TinyLength : spn + 1); c += bp)
  arr[c] |= 1;
  }
  arr.reduce((s, v, i) => { // accumulate sums...
const ns = s + (v ^ 1); arr[i] = ns; return ns; }, 0);
  return arr;
}();  

function tinyPhi(m) {
  const d = Math.trunc(m / TinyProduct);
  const ti = (m - d * TinyProduct + 1) >>> 1;
  const t = ti < TinyLength
          ? TinyTotients[ti]
          : TinyTotient - TinyTotients[TinyHalfProduct - ti];
  return d * TinyTotient + t;
}

function *countPrimesTo(limit) {
  if (limit <= WHLPRMS[WHLPRMS.length - 1]) {
let cnt = 0; for (let p of WHLPRMS) { if (p > limit) break; else ++cnt; }
return cnt; }

  const bpras = makeBasePrimeRepArrs();
  if (limit < 1024**2 + 3) { // for limit < about a million, just sieve...
let p = 3; let cnt = WHLPRMS.length;
for (let bpra of bpras())
  for (let bpr of bpra) { // just count base prime values to limit
    p += bpr + bpr; if (p > limit) return cnt; ++cnt; }
  }

  if (limit <= 32 * 2 * PGSZBITS + 3) { // count sieve to about 32 million...
const lmti = (limit - 3) / 2;
let cnt = WHLPRMS.length; // just use page counting to limit as per usual...
for (let pg of soePages(PGSZBITS, bpras)()) {
  const nxti = pg.lwi + (pg.sb.length << 3);
  if (nxti > lmti) { cnt += countPageFromTo(0, lmti - pg.lwi, pg.sb); break; }
  cnt += countPageFromTo(0, PGSZBITS - 1, pg.sb);
}
return cnt;
  }

  // Actual LMO prime counting code starts here...
  const sqrt = Math.trunc(Math.sqrt(limit)) >>> 0;
  const cbrt = Math.trunc(Math.cbrt(limit)) >>> 0;
  const sqrtcbrt = Math.trunc(Math.sqrt(cbrt)) >>> 0;
  const top = Math.trunc(limit / cbrt); // sized for maximum required!
  const bsprms = function() {
let bp = 3; let cnt = WHLPRMS.length + 1; for (let bpra of bpras())
  for (let bpr of bpra) {
    bp += bpr + bpr; if (bp > cbrt) return new Uint32Array(cnt); ++cnt; }
  }();
  bsprms.set(WHLPRMS, 1); // index zero not used == 0!
  const pisqrtcbrt = function() {
let cnt = WHLPRMS.length; let i = cnt + 1; let bp = 3;
stop: for (let bpra of bpras())
  for (let bpr of bpra) {
    bp += bpr + bpr; if (bp > cbrt) break stop;
    if (bp <= sqrtcbrt) ++cnt; bsprms[i++] = bp >>> 0; }
return cnt;
  }();
  const pis = function() { // starts with index 0!
const arr = new Uint32Array(cbrt + 2); let j = 0;
for (let i = 1; i < bsprms.length; ++i)
  for (; j < bsprms[i]; ) arr[j++] = (i - 1) >>> 0;
for (; j < arr.length; ) arr[j++] = (bsprms.length - 1) >>> 0;
return arr;
  }();
  const phis = function() { // index below TinyPhi degree never used...
const arr = (new Array(bsprms.length)).fill(1);
arr[0] = 0; arr[1] = 3; arr[2] = 2; // unused
for (let i = WHLPRMS.length + 2; i < arr.length; ++i) {
  arr[i] -= i - WHLPRMS.length - 1; } // account for non phi primes!
return arr;
  }();
  // indexed by `m`, contains lpf and Moebius value bit; one is negative...
  const specialroots = new Uint16Array(cbrt + 1); // filled in with S1 below...
  const S1 = function() { // it is very easy to calculate S1 recursively...
let s1acc = tinyPhi(limit);
function level(lpfni, lmtlpfni, mfv, m) {
  while (lpfni < lmtlpfni) {
    const pn = bsprms[lpfni]; const nm = m * pn;
    if (nm > cbrt) { // don't split, found S2 root leaf...
      specialroots[m] = (lmtlpfni << 1) | (mfv < 0 ? 1 : 0); return; }
    else { // recurse for S1; never more than 11 levels deep...
      s1acc += mfv * tinyPhi(Math.trunc(limit / nm)); // down level...
      level(9, lpfni, -mfv, nm); // Moebius sign change on down level!
      ++lpfni; } // up prime value, same level!
  }
}
level(9, bsprms.length, -1, 1); return s1acc;
  }();

  // at last, calculate the more complex parts of the final answer:
  function *complex() {
let s2acc = 0; let p2acc = 0; let p2cnt = 0; // for "P2" calculation
const buf = new Uint8Array(PGSZBITS >>> 3); let ttlcnt = 0;
const cnts = new Uint8Array(PGSZBITS >>> 9);
const cntaccs = new Uint32Array(cnts.length);
const revgen = revPrimesFrom(sqrt, bpras);
let rp = revgen.next().value; let p2v = Math.trunc(limit / rp);
const lwilmt = Math.trunc((top - 3) / 2);

const updtmsk = ((PGSZBITS << 3) - 1) >>> 0;
let nxttm = Date.now() + 1000;
for (let lwi = 0; lwi <= lwilmt; lwi += PGSZBITS) { // for all pages
  if ((lwi & updtmsk) == updtmsk && Date.now() >= nxttm) {
    nxttm = Date.now() + 1000; yield lwi / lwilmt * 100.0 };
  let pgcnt = 0; const low = 3 + lwi + lwi;
  const high = Math.min(low + (PGSZBITS << 1) - 2, top);
  let cntstrti = 0 >>> 0;
  function countTo(stop) {
    const cntwrd = stop >>> 9; const bsndx = stop & (-512);
    const xtr = countPageFromTo(bsndx, stop, buf);
    while (cntstrti < cntwrd) {
      const ncnt = cntaccs[cntstrti] + cnts[cntstrti];
      cntaccs[++cntstrti] = ncnt; }
    return cntaccs[cntwrd] + xtr;
  }
  const bpilmt = pis[Math.trunc(Math.sqrt(high)) >>> 0] >>> 0;
  const maxbpi = pis[Math.min(cbrt, Math.trunc(Math.sqrt(limit/low)))]>>>0;
  const tminbpi = pis[Math.min(Math.trunc(top / (high + 2)),
                               bsprms[maxbpi]) >>> 0];
  const minbpi = Math.max(TinyPrimes.length, tminbpi) + 1;
  fillPage(lwi, buf); let bpi = (WHLPRMS.length + 1) >>> 0;
 
  if (minbpi <= maxbpi) { // jump to doing P2 if not range

    // for bpi < minbpi there are no special leaves...
    for (; bpi < minbpi; ++bpi) { // eliminate all Tiny Phi primes...
      const bp = bsprms[bpi]; const i = (bp - 3) >>> 1; // cull base primes!
      phis[bpi] += countPageFromTo(0, PGSZBITS - 1, buf);
      partialSievePage(lwi, bp, buf); }
    for (let i = 0; i < cnts.length; ++i) { // init cnts arr...
      const s = i << 9; const c = countPageFromTo(s, s + 511, buf);
      cnts[i] = c; pgcnt += c; }

    // for all base prime values up to limit**(1/6) in the page,
    // add all special leaves composed of this base prime value and
    // any number of other higher base primes, all different,
    // that qualify as special leaves...
    let brkchkr = false;
    for (; bpi <= Math.min(pisqrtcbrt, maxbpi) >>> 0; ++bpi) {
      const bp = bsprms[bpi];
      const minm = Math.max(Math.trunc(limit / (bp * (high + 2))),
                            Math.trunc(cbrt / bp)) >>> 0;
      const maxm = Math.min(Math.trunc(limit / (bp * low)), cbrt) >>> 0;
      if (bp >= maxm) { brkchkr = true; break; }
      for (let m = maxm; m > minm; --m) {
        const rt = specialroots[m];
        if (rt != 0 && bpi < rt >>> 1) {
          const stop = Math.trunc(limit / (bp * m) - low) >>> 1;
          const mu = ((rt & 1) << 1) - 1; // one bit means negative!
          s2acc -= mu * (phis[bpi] + countTo(stop));
        } }
      phis[bpi] += pgcnt; // update intermediate base prime counters
      pgcnt -= partialSieveCountPage(lwi, bp, cnts, buf);
      cntstrti = 0; cntaccs[0] = 0;
    }
    // for all base prime values > limit**(1/6) in the page,
    // add results of all special levaes compoaed using only two primes...
    if (!brkchkr)
    for (; bpi <= maxbpi; ++bpi) {
      const bp = bsprms[bpi];
      let l = pis[Math.min(Math.trunc(limit / (bp * low)), cbrt)>>>0]>>>0;
      if (bp >= bsprms[l]) break;
      const piminm = pis[Math.max(Math.trunc(limit / (bp * (high + 2))),
                                  bp) >>> 0] >>> 0;
      for (; l > piminm; --l) {          
        const stop = Math.trunc(limit / (bp * bsprms[l]) - low) >>> 1;
        s2acc += phis[bpi] + countTo(stop);
      }
      phis[bpi] += pgcnt; // update intermediate base prime counters
      if (bpi <= bpilmt) {
        pgcnt -= partialSieveCountPage(lwi, bp, cnts, buf);
        cntstrti = 0; cntaccs[0] = 0; }
    }
  }

  // complete cull page segment, then count up "P2" terms in range...
  for (; bpi <= bpilmt; ++bpi) partialSievePage(lwi, bsprms[bpi], buf);
  let ndx = 0 >>> 0;
  while (p2v >= low && p2v < high + 2) {
    const nndx = (p2v - low) >>> 1;
    ++p2cnt; ttlcnt += countPageFromTo(ndx, nndx, buf);
    p2acc += ttlcnt;
    ndx = (nndx + 1) >>> 0; p2v = Math.trunc(limit / revgen.next().value);
  }
  if (ndx < PGSZBITS) ttlcnt += countPageFromTo(ndx, PGSZBITS - 1, buf);
}
// adjust for now known delta picbrt to pisqrt!
p2acc -= p2cnt * (p2cnt - 1) / 2 +
           p2cnt * (bsprms.length - 1 - WHLPRMS.length);
const Piy = bsprms.length - 1;
return S1 + s2acc - p2acc + Piy - 1;
  }
  const gen = complex(); let lst = gen.next();
  for (; !lst.done; lst = gen.next()) yield lst.value;
  return lst.value;
}

let cancelled = false;

function doit() {
  const limit =  Math.floor(parseFloat(document.getElementById('limit').value));
  const start = Date.now();
  const pggen = countPrimesTo(limit);
  function pgfnc() {
if (cancelled) {            
  document.getElementById('output').innerText = "Cancelled!!!";
}
else {
  const pgrslt = pggen.next();
  if (!pgrslt.done) {
    // value is a percent done...            
    document.getElementById('output').innerText = "Sieved " + (pgrslt.value.toFixed(3)) + "%";
    setTimeout(pgfnc, 7); return;
  }
  // value is the count result...
  const elpsd = Date.now() - start;
  document.getElementById('output').innerText = "Found " + pgrslt.value 
+ " primes up to " + limit + " in " + elpsd + " milliseconds.";      
}
cancelled = false;    
document.getElementById('go').onclick = strtclick;
document.getElementById('go').value = "Start Sieve...";            
document.getElementById('go').disabled = false;
  }
  pgfnc();
}

const cancelclick = function () {
  cancelled = true;
  document.getElementById('go').disabled = true;
  document.getElementById('go').value = "Cancelled!!!";
  document.getElementById('go').onclick = strtclick;
}

const strtclick = function () {
  const limit =  Math.floor(parseFloat(document.getElementById('limit').value));
  if (!Number.isInteger(limit) || (limit < 0) || (limit > MAXVALUE)) {
    
  document.getElementById('output').innerText = "Top limit must be an integer between 0 and " + MAXVALUE + "!";
return;
  }
  document.getElementById('output').innerText = "Sieved 0%";
  document.getElementById('go').onclick = cancelclick;
  document.getElementById('go').value = "Running, click to cancel...";
  cancelled = false;
  setTimeout(doit, 7);
};

document.getElementById('go').onclick = strtclick;
html,
body {
  justify-content: center;
  align-items: center;
  text-align: center;
  font-weight: bold;
  font-size: 120%;
  margin-bottom: 10px;
}

.title {
  font-size: 150%;
}

.input {
  font-size: 100%;
  padding:5px 15px 5px 15px;
}

.output {
  padding:7px 15px 7px 15px;
}

.doit {
  font-weight: bold;
  font-size: 110%;
  border:3px solid black;
  background:#F0E5D1;
  padding:7px 15px 7px 15px;
}
<!doctype html>
<html>
  <head>
<meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Legarias-Miller-Odzlinko Prime Counting Function...</title>
  </head>
  <body>
<div class="title">
  <text>
    Legarias-Miller-Odzlinko Prime Counting Function
  </text>
</div>
<div>
  <text>
    Top counting limit value:
  </text>
  <input class="input" type="textbox" id="limit" value="1e9" />
</div>
<div class="output">
  <text>The enforced limit is zero to 9007199254740991 (2**53-1).</text>
</div>
<div class="output">
  <text>Refresh the page to reset to default values or stop processing!</text>
</div>
<div class="output">
  <text id="output">
      Waiting to start...
  </text>
</div>
<div>
  <input class="doit" type="button" id="go" value="Start counting..." />
</div>
  </body>
</html>

在我的英特尔 Skylake i5-6500 CPU 上运行 Google Chrome 版本 96,频率为 3.6 GHz(单线程提升率),上面的代码可以按如下方式计算素数范围:

limit count time (seconds)
1e9 50847534 0.006
1e10 455052511 0.016
1e11 4118054813 0.055
1e12 37607912018 0.234
1e13 346065536839 1.117
1e14 3204941750802 5.595
1e15 29844570422669 28.434
2**53-1 (about 9e15) 252252704148404 140.219

请注意,当在 node.js 下运行时,会有大约 0.1 秒的额外初始化延迟,但在 node.js 版本 16.6.1 下运行会更快一些(可能是 3%)。

【讨论】:

  • 有趣的是,原始 LMO 作者的程序在他们的 IBM 3081 Model K 大型计算机(双核,38 MHz,使用约 30 千瓦)上运行,最后一次基本(非 alpha)计算将运行到大约 9e15如果它是单核运行的,大约需要 120,000 秒,这使得该程序在当前普通台式计算机上以 JavaScript 单核运行大约快一千倍,而时钟速度的比率只有一百倍左右。这显示了现代 CPU 的更高效执行以及我的实现效率的提高。
  • 可以通过更复杂的 SoE 进行进一步的工作以使筛分计算更快:例如,如果我的 SO 答案 developing a maximally factorized page-segmented SoE in JavaScript 适合此用途,它将减少筛分操作的数量大约 2.5 倍,应该使上述技术几乎这个比率更快,并且仍然是基本的 LMO 算法,但我在这里没有这样做,因为它会增加几百行代码量并且不适合这个答案。
  • 惊人的工作!绝对值得拥有自己的存储库...
  • @Ovinus Real,谢谢 - 我通常启动存储库的规则比 SO 答案大,因为这个答案包括代码和 README.md 文件中的内容,它仍然远远低于这个限制。它可能变得足够大以保证回购的唯一方法是,如果我要超越基本的 LMO,我不太可能在 JavaScript 中这样做,因为它不是我更喜欢使用的语言;我在这里使用它的原因是能够在答案本身中获得一个可运行的 sn-p。
【解决方案2】:

派对有点晚了,但这在 O(n^2/3) 时间和空间内有效(请参阅下面 GordonBGood 的评论)。

function eratosthenesWithPi(n) {
  let array = [], upperLimit = Math.sqrt(n), output = [];
  let pi = [0, 0]

  for (let i = 0; i < n; i++) {
    array.push(true);
  }

  for (let i = 2; i <= upperLimit; i++) {
    if (array[i]) {
      for (var j = i * i; j < n; j += i) {
        array[j] = false;
      }
    }
  }

  let cnt = 0

  for (let i = 2; i < n; i++) {
    if (array[i]) {
      output.push(i);
      cnt++
    }

    pi.push(cnt)
  }

  return {primes: new Uint32Array(output), pi: new Uint32Array(pi)}
}

const phiMemo = []
let primes = []

function Phi(m, b) {
  if (b === 0)
    return m
  if (m === 0)
    return 0

  if (m >= 800) {
    return Phi(m, b - 1) - Phi(Math.floor(m / primes[b - 1]), b - 1)
  }

  let t = b * 800 + m

  if (!phiMemo[t]) {
    phiMemo[t] = Phi(m, b - 1) - Phi(Math.floor(m / primes[b - 1]), b - 1)
  }

  return phiMemo[t]
}

const smallValues = [0, 0, 1, 2, 2, 3]
let piValues

function primeCountingFunction(x) {
  if (x < 6)
    return smallValues[x]

  let root2 = Math.floor(Math.sqrt(x))
  let root3 = Math.floor(x ** (1/3))

  let top = Math.floor(x / root3) + 1

  if (root2 + 1 >= primes.length) {
    let res = eratosthenesWithPi(top + 2)

    primes = res.primes
    piValues = res.pi
  }

  let a = piValues[root3 + 1], b = piValues[root2 + 1]

  let sum = 0

  for (let i = a; i < b; ++i) {
    let p = primes[i]

    sum += piValues[Math.floor(x / p)] - piValues[p] + 1
  }

  let phi = Phi(x, a)

  return phi + a - 1 - sum
}

console.log(primeCountingFunction(1e8))

在 JSFiddle 上试用:https://jsfiddle.net/vo0g274f/1/

对我来说,这大约需要 31 毫秒。我正在研究一种更节省空间的方法 atm,完成后我会在这里发布。

【讨论】:

  • 这是一个非常好的解决方案。您是否设法创建了您提到的more space-efficient method
  • 抱歉,但这个答案不是O(n^(2/3)),而是O(n/((ln n)^3)),在渐近时间复杂度中,由于1/((ln x)^ 3)的因素,它对于“较小”的范围来说似乎很快对于 Meissel 素数计数函数的这个简单实现,范围和缓存小值“Phi”的“恒定偏移”优化;将素数计数到万亿/1e12 几乎需要一分钟,并且由于此实现中使用的埃拉托色尼筛 (SOE) 筛分的幼稚实现使用过多的内存而限制了范围。
  • @GordonBGood 感谢您的更正!实现更快的方法会很酷,但此时最好将一些经过良好测试的 C++ 库(如 primecount)编译为 WASM
  • @Ovinus Real,我正在考虑是否值得一个相当简单的东西(例如 LMO)的 JavaScript 版本。是的,可以通过将Kim Walich's primecount 的 LMO 部分编译为 WASM 来做到这一点,但我不确定它是否足够小以作为 SO 答案发布,也不会那么容易让人们使用并可能理解.我可以解决这个问题...
  • @GordonB祝你好运!我认为这绝对值得;一般来说,我希望看到更好的 JS 数学函数,而不仅仅是从 WASM 编译的。例如,我一直在实现二进制任意精度浮点 (github.com/anematode/grapheme/blob/master/src/math/arb/bigfloat.js) 以取代 decimal.js。不确定是否有特别高的需求,但我知道我会使用快速素数计数功能。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-05-14
  • 1970-01-01
  • 1970-01-01
  • 2017-06-26
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多