Sieve Methods Beyond Primes: A Unified View of Linear Sieve
Based on original number theory notes, covering prime sieves, Euler's totient function, and Wilson's theorem.
Contents#
Prime Sieve MethodsEuler's Totient FunctionWilson's Theorem
Prime Sieve Methods#
Linear Sieve Method#
The Linear Sieve Method (also known as the Euler’s Sieve) is the most commonly used!
void init(int n) {
for (int i = 2; i <= n; ++i) {
if (!vis[i]) {
pri[cnt++] = i;
}
for (int j = 0; j < cnt; ++j) {
if (1ll * i * pri[j] > n) break;
vis[i * pri[j]] = 1;
if (i % pri[j] == 0) {
// i % pri[j] == 0
// In other words, i was previously sieved by pri[j]
// Since primes in pri are in ascending order, the result of multiplying i by any other prime
// will definitely be sieved later by a multiple of pri[j],
// so there's no need to sieve it here. Hence, break directly.
break;
}
}
}
}cppSieve of Eratosthenes#
The Sieve of Eratosthenes is简称 the Eratosthenes Sieve. Its time complexity is .
bool is_prime[N];
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; // prime[p] is i, post-increment represents the current prime count
if ((long long)i * i <= n)
for (int j = i * i; j <= n; j += i)
// Since we've already sieved multiples of numbers from 2 to i-1, we start directly from
// the square of i, which improves the running speed
is_prime[j] = 0; // Multiples of i are not primes
}
}
return p;
}cppSieving up to the square root achieves
bool is_prime[N];
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;
// i * i <= n means i <= sqrt(n)
for (int i = 2; i * i <= n; ++i) {
if (is_prime[i]) {
prime[p++] = i;
for (int j = i * i; j <= n; j += i) is_prime[j] = 0;
}
}
return p;
}cpp1 Optimized Version#
Sieve only odd numbers (Because all even numbers except 2 are composite, we can skip them directly and only care about odd numbers.)
First, this halves our memory requirement; second, the required operations are approximately halved.
#include<iostream>
#include<vector>
using namespace std;
vector<int> sieve(int n)
{
vector<bool> a(n + 1, 1);
vector<int> b;
b.push_back(2);
for (int i = 3; i <= (int)sqrt(n); i += 2)
for (int j = i * i; j <= n; j += i)
a[j] = false;
for (int i = 2; i <= n; i++)
if (i % 2 != 0 && a[i])
b.push_back(i);
return b;
}cppSegmented Sieve#
Uses very little memory!!! The following implementation uses a segmented sieve to count the number of primes less than or equal to n.
int count_primes(int n) {
const int S = 10000;
vector<int> primes;
int nsqrt = sqrt(n);
vector<char> is_prime(nsqrt + 1, true);
for (int i = 2; i <= nsqrt; i++) {
if (is_prime[i]) {
primes.push_back(i);
for (int j = i * i; j <= nsqrt; j += i) is_prime[j] = false;
}
}
int result = 0;
vector<char> block(S);
for (int k = 0; k * S <= n; k++) {
fill(block.begin(), block.end(), true);
int start = k * S;
for (int p : primes) {
int start_idx = (start + p - 1) / p;
int j = max(start_idx, p) * p - start;
for (; j < S; j += p) block[j] = false;
}
if (k == 0) block[0] = block[1] = false;
for (int i = 0; i < S && start + i <= n; i++) {
if (block[i]) result++;
}
}
return result;
}cppThe asymptotic time complexity of the segmented sieve is the same as the Sieve of Eratosthenes (unless the block is very small), but the required memory is reduced to , and it has better cache performance. On the other hand, for each pair of block and primes in the interval , division is required, which is much worse for smaller blocks. Therefore, a balance must be struck when choosing the constant S.
Taking the block size between and yields the best speed.
Euler’s Totient Function#
- Related Article ↗ (in Chinese)
Euler’s Totient Function:#
Denoted as , it represents the count of numbers less than or equal to that are coprime to .
Properties:
-
Euler’s totient function is multiplicative. If , then . Particularly, when is odd, .
-
.
-
If , where is prime, then . (Follows from the definition)
-
From the unique factorization theorem, let , where are primes, then .
Implementation:
Euler’s Totient for a Single Number:#
#include <cmath>
int euler_phi(int n) {
int ans = n;
for (int i = 2; i * i <= n; i++)
if (n % i == 0) {
ans = ans / i * (i - 1);
while (n % i == 0) n /= i;
}
if (n > 1) ans = ans / n * (n - 1);
return ans;
}cppSieve for Euler’s Totient Function:#
We have:
Modified based on the linear sieve:
vector<int> minp, primes, phi;
void sieve(int n) {
minp.assign(n + 1, 0);
phi.assign(n + 1, 0);
primes.clear();
phi[1] = 1;
for (int i = 2; i <= n; i++) {
if (minp[i] == 0) {
minp[i] = i;
primes.push_back(i);
phi[i] = i - 1;
}
for (auto p : primes) {
if (i * p > n) break;
minp[i * p] = p;
if (p == minp[i]) {
phi[i * p] = phi[i] * p;
break;
}
phi[i * p] = phi[i] * phi[p];
}
}
}cppPractice Problems#
P2568 GCD ↗#
Given a positive integer , find the number of pairs such that and is prime.
That is, find
#define int long long
vector<int> minp, primes, phi;
signed main() {
int n;cin >> n;
sieve(n);
phi[1] = 1;
partial_sum(phi.begin(), phi.end(), phi.begin());
int ans = 0;
for (auto p : primes) {
ans += 2 * phi[n / p] - 1;
}
cout << ans << '\n';
}cppP2398 GCD SUM ↗#
Find
For
The count of pairs with is
So iterate over and sum the cases where .
#define int long long
signed main() {
int n;cin >> n;
sieve(n);
partial_sum(phi.begin(), phi.end(), phi.begin());
int ans = 0;
for (int i = 1;i <= n;i++) {
ans += i * (2 * phi[n / i] - 1);
}
cout << ans << '\n';
}cppP2158 Guard of Honor ↗#
The problem requires , with .
Then . Find the number of pairs . We can first subtract 1 from (which effectively shifts the coordinate origin one unit up and right), then handle the special points and separately. The answer becomes:
#define int long long
signed main() {
ios::sync_with_stdio(false), cin.tie(nullptr);
int n;cin >> n;
sieve(n);
if (n == 1) {
cout << "0\n";return 0;
}
partial_sum(phi.begin(), phi.end(), phi.begin());
cout << phi[n - 1] * 2 + 1 << '\n';
}cppEuler’s Theorem:#
Statement: If , then .
Fermat’s Little Theorem can be seen as a special case of Euler’s Theorem when is a prime .
Extended Euler’s Theorem:#
Extended Euler’s Theorem ↗ content:
Wilson’s Theorem#
1 Wilson’s Theorem#
Statement: For a prime ,
Corollary:
Algorithm for computing remainder:
Compute % .
Time Complexity: . If the function needs to be called multiple times, precomputation can be done outside the function, making the time to compute equal to
int factmod(int n, int p) {
vector<int> f(p);
f[0] = 1;
for (int i = 1; i < p; i++) f[i] = f[i - 1] * i % p;
int res = 1;
while (n > 1) {
if ((n / p) % 2) res = p - res;
res = res * f[n % p] % p;
n /= p;
}
return res;
}cpp2 Legendre’s Formula:#
The exponent of a prime in , denoted , is given by:
where is the sum of the digits of when expressed in base .
Specifically, the exponent of 2 in a factorial is
implementation:
int multiplicity_factorial(int n, int p) {
int count = 0;
do {
n /= p;
count += n;
} while (n);
return count;
}cpp3 Kummer’s Theorem:#
The exponent of a prime in the binomial coefficient is exactly the number of carries when adding and in base (or equivalently, when subtracting from in base ).
That is,
Specifically, the exponent of 2 in a binomial coefficient is
4 Generalization of Wilson’s Theorem:#
For a prime and a positive integer , we have