Prime Sieving
link
0. Lời dẫn đầu
Note này tổng hợp lại các thuật toán về sàng nguyên tố, sắp xếp dựa trên tốc độ của thuật, code và tối ưu hỏa bởi SPyofgame.
Note này nhằm tới những code quá dài và phức tạp. Mặc dù có những code rất khó hiểu.
Bạn có thể xem tốc độ chay của các thuật toán tại đây
Code không có bản quyền và cho phép thoải mái sử dụng nhưng xin đừng sửa nguồn (Original) nếu có
Bạn có thể đóng góp ý kiện tại mục comment. Mình rất vui lòng phản hồi và cập nhật blog
Trong tương lai xa mình sẽ:
- Thêm hướng dẫn đầy đủ, cụ thể và chứng minh cho từng thuật sàng
- Thêm comment cho code
- Chia làm 2 note tiếng Việt với tiếng Anh
- Thêm một số loại sàng khác
- Thêm benchmark chó việc lấy số nguyên tố
1. Brute-forces
Với mọi số nguyên trong đoạn . Ta đếm số ước của trong đoạn . Nếu có đúng ước thì là số nguyên tố
Time:
Space:
Algo: Brute-forces Sieve
2.Trial Division
Nhận thấy là là số nguyên tố, khi và chỉ khi nó có đúng ước nguyên dương là và
Nếu và tồn tai một giá trị trong đoạn mà chia hết thì sẽ là hợp số
Thấy rằng khi tồn tại giá trị như thế, thì tồn tại số nguyên để
Không mất tính tổng quát ta giả sử
Áp dụng bất đẳng thức AM-GM cho 2 số nguyên dương và ta có
Từ đó suy ra ta chỉ cần duyêt qua
Time:
Space:
Algo: Sqrt Sieve
3.Improved Trial Division
Nhận xét các số có dạng với và và
- Khi thì chia hết cho không phải số nguyên tố.
- Khi thì chia hết cho không phải số nguyên tố.
- Vậy ta chỉ cần kiểm tra các số thuộc dạng và
Ta áp dụng tính chất trên cho cả việc sàng lẫn việc kiểm tra
For
Time: total - check
Space:
Algo: Sqrt Sieve
4.Second Improved Trial Division
Ta nhận thấy nếu không phải số nguyên tố thì tồn tại một số mà chia hết.
Mà mọi ước của đều là ước của
Nên ta chỉ cần kiểm tra xem có ước nguyên tố nào hay không
Time:
Space:
Algo: Sqrt Sieve
5.Divisor Sieve
Với mỗi số . Đánh dấu các bội của khác gồm các số () không phải số nguyên tố. Chứng minh:
- Vì các số có dạng và nên có ít nhất ước là nên không phải số nguyên tố
Time:
Space:
Algo: Divisor Sieve
6.Sundaram Sieve
Time:
Space:
Algo: Divisor Sieve
7.Eratosthenes Sieve
Nhận thấy rằng các số có nhiều ước thì sẽ bị đánh dấu nhiều lần. Vì khi là hợp số, thì với mọi ước của , thì bội của cũng là bội của . Nên để hạn chế điều đó, ta chỉ đánh dấu các bội của khi là số nguyên tố.
Time:
Space:
Algo: Eratosthenes Sieve
8.Improved Eratosthenes Sieve
Mình có thể cải thiện hơn nữa. Bằng cách đánh dấu các số chẵn khác không phải số nguyên tố. Rồi xét các số lẻ
Thay vì đánh dấu các bội với , thì nhận thấy mọi với đều đã bị đánh dấu bởi một số nguyên tố nào đó trước . Nên ta đánh dấu từ luôn.
Time:
Space:
Algo: Eratosthenes Sieve
9.Linear Eratosthenes Sieve
Nhưng, với cách trên thì mình vẫn phải đánh dấu nhiều lần các số có nhiều ước nguyên tố.
Ta gọi lpf[x]
là ước nguyên tố nhỏ nhất của (viết tắt của lowest_prime_factor
)
Khi này mọi số đều có thể được biểu diễn bằng . Mà từ định nghĩa ta có
Giờ nếu ta chỉ đánh dấu các số có dạng với nguyên tố và . Thì mỗi số chỉ bị đánh dấu bởi một số nguyên tố mà thôi
Time:
Space: lpf[] + prime[]
Algo: Eratosthenes Sieve
10.Improved Linear Eratosthenes Sieve
Mình biết trước được số lượng phần tử rồi thì có thể sài mảng tĩnh để xử lí nhanh hơn
Time:
Space: lpf[] + prime[]
Algo: Eratosthenes Sieve
11.Atkin Sieve
Cái thuật sàng này thì phức tạp hơn nhiều và cũng sử dụng nhiều phép toán nhân chia nên khó tối ưu hơn là sàng Erastothenes.
Nhưng mà thôi kệ, cứ viết về nó vậy.
Ý tưởng của A. O. L. Atkin và Daniel J. Bernstein là như sau:
- Một số nguyên dương không chính phương là số nguyên tố khi và chỉ khi có lẻ cặp nghiệm nguyên dương
Để phủ hết các số nguyên tố ta kiểm tra thêm các dạng
- xét phương trình với
- xét phương trình với
- xét phương trình với
Time:
Space:
Algo: Atkin Sieve
12.Improved Atkin Sieve
Ta chia làm các vòng for độc lập để có thể có những tối ưu riêng từng cái
Time:
Space:
Algo: Atkin Sieve
13.Non-modulo Atkin Sieve
Áp dụng toán học và tìm những trường hợp nào nghiệm thỏa cả phương trình lẫn điều kiện modulo và đơn giản hóa lại thành phép cộng trừ ta thu được như sau
Time:
Space:
Algo: Atkin Sieve
14.Improved Non-modulo Atkin Sieve
Time:
Space:
Algo: Atkin Sieve
15.Eratosthenes Wheel-2 Sieve
Time:
Space:
Algo: Erastothenes Sieve
16.Improved Eratosthenes Wheel-2 Sieve
Time:
Space:
Algo: Erastothenes Sieve
17.Eratosthenes Wheel-2 Bitwise Sieve
Time:
Space:
Algo: Erastothenes Sieve
18.Improved Eratosthenes Wheel-2 Bitwise Sieve
Time:
Space:
Algo: Erastothenes Sieve
19.Improved Erastothenes Wheel-2-3-5-7 Sieve
Time:
Space:
Algo: Erastothenes Sieve
20.Improved Erastothenes Wheel-2-3-5-7 Segment Sieve
Time:
Space:
Algo: Erastothenes Sieve
#include <algorithm>
#include <iostream>
#include <cstring>
#include <vector>
#include <cmath>
using namespace std;
typedef unsigned int uint;
constexpr uint LIM = 1e6;
constexpr uint NEXT8[] = {1, 2, 3, 4, 5, 6, 7, 0};
constexpr uint WHEEL_1[] = {2, 1, 2, 1, 2, 3, 1, 3};
constexpr uint WHEEL_0[] = {1, 0, 1, 0, 1, 2, 0, 2};
constexpr uint WHEEL_POS[] = {0, 2, 3, 5, 6, 8, 11, 12};
constexpr uint WHEEL_IDX[] = {0, 0, 1, 2, 2, 3, 4, 4, 5, 5, 5, 6, 7, 7, 7, 0, 0, 1, 2, 2, 3, 4, 4, 5, 5, 5, 6, 7};
constexpr uint WHEEL_DUP[] = {0, 2, 2, 3, 5, 5, 6, 8, 8, 11, 11, 11, 12, 15, 15, 15, 17, 17, 18, 20, 20, 21, 23, 23, 26, 26, 26, 27};
constexpr uint L1CACHEPOW = 14u + 3u;
constexpr uint L1CACHESZ = (1u << (int)L1CACHEPOW);
constexpr uint SEGSZ = L1CACHESZ / 15u * 15u;
constexpr uint PRESZ = (65535u - 7u) / 2u;
bool isPrime[PRESZ + 1];
bool segPrime[SEGSZ];
uint cntPrime;
void sieve(uint n)
{
cntPrime = 0u; if (n < 2u) return;
cntPrime = 1u; if (n < 3u) return;
cntPrime = 2u; if (n < 5u) return;
cntPrime = 3u; if (n < 7u) return;
const uint upper = (n - 7u) / 2u;
uint sqrt_upper = (sqrt(n) - 7u) / 2u;
memset(isPrime, true, sizeof(isPrime));
for (uint i = 0u; i <= 124u; ++i) if (isPrime[i])
{
uint p = 7u + i + i;
for (uint j = (p * p - 7u) / 2u; j <= PRESZ; j += p)
isPrime[j] = false;
}
for (uint i = 0u, w = 0u, si = 0u; i <= upper; i += WHEEL_1[w], si += WHEEL_1[w], si = (si >= SEGSZ) ? 0u : si, w = NEXT8[w])
{
if (si == 0u)
{
memset(segPrime, true, sizeof(segPrime));
for (uint j = 0u, bw = 0u; j <= PRESZ; j += WHEEL_1[bw], bw = NEXT8[bw]) if (isPrime[j])
{
uint p = 7u + j + j;
uint k = p * (j + 3u) + j;
if (k >= i + SEGSZ) break;
uint pa[3] = {p, p + p, p + p + p};
uint sw = bw;
if (k < i)
{
k = (i - k);
k %= (15u * p);
if (k != 0)
{
uint os = WHEEL_POS[bw];
sw = os + ((k + p - 1u) / p);
sw = WHEEL_DUP[sw];
k = (sw - os) * p - k;
sw = WHEEL_IDX[sw];
}
}
else k -= i;
for (; k < SEGSZ; k += pa[WHEEL_0[sw]], sw = NEXT8[sw])
segPrime[k] = false;
}
}
if (segPrime[si]) ++cntPrime;
}
}
int main()
{
int n;
cin >> n;
sieve(n);
cout << "Number of primes up to " << n << " is " << cntPrime;
return 0;
}
21.Erastothenes Segment Sieve
Time:
Space:
Algo: Erastothenes Sieve
22.Improved Erastothenes Segment Sieve
Time:
Space:
Algo: Erastothenes Sieve
23.Erastothenes Segment Bitwise Sieve
Time:
Space:
Algo: Erastothenes Sieve
Original: primesieve
24.Improved Erastothenes Segment Bitwise Sieve
Time:
Space:
Algo: Erastothenes Sieve
Original: primesieve
#include <algorithm>
#include <iostream>
#include <cstring>
#include <vector>
#include <cmath>
using namespace std;
typedef unsigned int uint;
constexpr uint unset_bit[8] = { uint(-2), uint(-2), uint(-3), uint(-3), uint(-5), uint(-5), uint(-9), uint(-9)};
constexpr uint popcnt[128] = {
0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5,
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6,
2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7,
};
constexpr uint LIM = 1e6;
constexpr uint SQRT_LIM = sqrt(LIM);
constexpr uint OPTIMAL_RANGE = 1 << 15;
constexpr uint offset[2] = {2, 4};
uint8_t mark[max(SQRT_LIM, OPTIMAL_RANGE)];
uint8_t is_prime[SQRT_LIM + 1];
uint cntPrime;
void sieve(uint lim)
{
cntPrime = 0;
if (lim <= 1) return ;
uint sqrt = std::sqrt(lim);
uint mark_size = std::max(sqrt, OPTIMAL_RANGE);
uint segment_size = mark_size * 8;
uint n = 3, s = 3;
bool tn = 1, ts = 1;
std::vector<uint> seg_prime;
std::vector<uint> seg_multi;
memset(is_prime, true, sizeof(is_prime));
for (uint low = 0; low <= lim; low += segment_size)
{
uint high = min(lim, low + segment_size - 1);
mark_size = ((high - low) >> 3) + 1;
fill_n(mark, mark_size, 0xf);
for (; n * n <= high; n += offset[tn ^= 1]) if (is_prime[n])
for (uint j = n * n; j <= sqrt; j += n)
is_prime[j] = false;
for (; s * s <= high; s += offset[ts ^= 1]) if (is_prime[s])
{
seg_prime.push_back(s);
seg_multi.push_back(s * s - low);
}
for (uint n = 0; n < seg_prime.size(); ++n)
{
uint j = seg_multi[n];
for (uint k = seg_prime[n] * 2; j < segment_size; j += k) mark[j >> 3] &= unset_bit[j & 7];
seg_multi[n] = j - segment_size;
}
if (high == lim) mark[mark_size - 1] &= ~(0xf << (((lim & 7) + 1) >> 1));
for (uint n = 0; n < mark_size; n++) cntPrime += popcnt[mark[n]];
}
}
int main()
{
int n;
cin >> n;
sieve(n);
cout << "Number of primes up to " << n << " is " << cntPrime;
return 0;
}