前言
很久以前,我出了区间质数,当然求$l\dots r$之间的质数等于$\pi®-\pi(l-1)$,其中$\pi(n)$为质数计数函数,表示$1\dots n$中质数的个数。
我最初给出的方法是分段打表+Miller Rabin素性判定,可以解决$n\le10^9$规模,但是更大的规模无法打表,因为内存不够……最近看到洛谷P3912后,zyy提供了LibreOJ #6235,求$\pi(n),n\le10^{11}$。对此我想大致了解一下质数计数算法。
筛法
计算$\pi(n)$最简单的想法就是筛法,这里同时来比较一下埃氏筛和欧拉筛的实际效率,以及bitset优化的效率。与算法竞赛不同的是,代码避免使用静态数组,而是根据输入规模动态分配内存。因此与静态数组的性能有一些差异。
埃氏筛
原始版本(era.cpp)
把$\sqrt n$内的质数的倍数筛掉,时间复杂度$O(n\log\log n)$,空间复杂度$O(n)$。一个小的优化是直接忽略偶数(除了2)。这里假设$n\ge2$。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #include <iostream> #include <algorithm> #include <ctime> using namespace std; typedef unsigned int_t; int main() { int_t n; cin >> n; clock_t start = clock(); bool *p = new bool[n + 1]; fill(p + 2, p + n + 1, true); for (int_t i = 3; i * i <= n; i += 2) if (p[i]) for (int_t j = i * i; j <= n; j += i + i) p[j] = false; int_t cnt = 1; for (int_t i = 3; i <= n; i += 2) cnt += p[i]; cout << cnt << endl; delete[] p; cout << clock() - start << endl; return 0; }
|
bitset(era_bitset.cpp)
值得注意的是,std::bitset是静态的模板,因此我实际上用了std::vector<bool>。时间复杂度不变,但是常数小,因为空间局部性吧。空间复杂度$O(\frac n8)$。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| #include <iostream> #include <vector> #include <ctime> using namespace std; typedef unsigned int_t; int main() { int_t n; cin >> n; clock_t start = clock(); vector<bool> p(n + 1, true); for (int_t i = 3; i * i <= n; i += 2) if (p[i]) for (int_t j = i * i; j <= n; j += i + i) p[j] = false; int_t cnt = 1; for (int_t i = 3; i <= n; i += 2) cnt += p[i]; cout << cnt << endl; cout << clock() - start << endl; return 0; }
|
分段筛(era_segmented.cpp)
我最早是在某篇论文上看到的,但是http://primesieve.org/segmented_sieve.html的介绍和代码很容易理解。先生成$\sqrt n$内的质数表,然后分段筛,段长度接近L1数据缓存。能很大程度提高效率,因为增加了缓存命中率。空间复杂度$O(\pi(\sqrt n)+B)$,$B$为段长度。
另外,这可以计算较小的区间内的质数,如$9.999*10^{11}\dots10^{12}$。这样取段大小$10^8$,应该可以通过LOJ那题。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| #include <iostream> #include <vector> #include <algorithm> #include <cmath> #include <ctime> using namespace std; typedef unsigned int_t; const int_t seg_size = 32768; int main() { int_t n; cin >> n; clock_t start = clock(); int_t m = sqrt(n); bool *p = new bool[m + 1]; fill(p + 2, p + m + 1, true); for (int_t i = 2; i * i <= m; i++) if (p[i]) for (int_t j = i * i; j <= m; j += i) p[j] = false; bool *buf = new bool[seg_size]; vector<int_t> primes; vector<int_t> nxt; int_t cnt = 1; for (int_t low = 0, s = 3, now = 3; low <= n; low += seg_size) { fill(buf, buf + seg_size, true); int_t high = min(low + seg_size - 1, n); for (; s * s <= high; s += 2) if (p[s]) { primes.push_back(s); nxt.push_back(s * s - low); } for (size_t i = 0; i < primes.size(); i++) { int_t j = nxt[i]; for (int_t k = primes[i] * 2; j < seg_size; j += k) buf[j] = false; nxt[i] = j - seg_size; } for (; now <= high; now += 2) cnt += buf[now - low]; } cout << cnt << endl; delete[] p; delete[] buf; cout << clock() - start << endl; return 0; }
|
欧拉筛
原始版本(euler.cpp)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <iostream> #include <vector> #include <algorithm> #include <ctime> using namespace std; typedef unsigned int_t; int main() { int_t n; cin >> n; clock_t start = clock(); bool *p = new bool[n + 1]; fill(p + 2, p + n + 1, true); vector<int_t> primes; for (int_t i = 3; i <= n; i += 2) { if (p[i]) primes.push_back(i); for (int_t j : primes) { if (i * j > n) break; p[i * j] = false; if (i % j == 0) break; } } cout << primes.size() + 1 << endl; delete[] p; cout << clock() - start << endl; return 0; }
|
bitset(euler_bitset.cpp)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #include <iostream> #include <vector> #include <ctime> using namespace std; typedef unsigned int_t; int main() { int_t n; cin >> n; clock_t start = clock(); vector<bool> p(n + 1, true); vector<int_t> primes; for (int_t i = 3; i <= n; i += 2) { if (p[i]) primes.push_back(i); for (int_t j : primes) { if (i * j > n) break; p[i * j] = false; if (i % j == 0) break; } } cout << primes.size() + 1 << endl; cout << clock() - start << endl; return 0; }
|
效率
开O2优化,用clock测量,取最小值,单位:毫秒。
$n$ |
$\pi(n)$ |
era |
era_bitset |
era_seg |
euler |
euler_bitset |
$10^7$ |
$664,579$ |
$79$ |
$20$ |
$12$ |
$53$ |
$44$ |
$10^8$ |
$5,761,455$ |
$1,014$ |
$538$ |
$91$ |
$621$ |
$492$ |
$10^9$ |
$50,847,534$ |
$11,752$ |
$7,131$ |
$1,194$ |
$6,533$ |
$5,309$ |