Incremental Sieve of Eratosthenes
The Sieve of Eratosthenes is a well known algorithm for computing primes, but suffers from space requirements. The Incremental Sieve solves that problem.
The Sieve works by walking through each of the numbers in the range. But that means that you have to store the numbers. Even if you reduce this to a bit map, the space requirement is O(n).
Thinking as a software engineer, it is evident that the core of the Sieve’s implementation would be a nested loop.
for each prime
for each multiple
mark number as composite
end
end
The inner loop depends on the outer loop. That is good for efficiency, but not for analysis. So lets rewrite as:
for each prime
for each number
if number is a multiple of prime
mark number as composite
end
end
end
Now we can see that we could get the same results by inverting the loops.
for each number
for each prime
if number is a multiple of prime
mark number as composite
end
end
end
So, now, we don’t have to store the numbers. Instead we only have to store the primes. And since we are storing primes, we should make the test store it.
for each number
for each prime
if number is a multiple of prime
go to next number
end
end
add number to list of primes
end
The remaining question is - how are we going implement that if
? We could
just divide. But division is slow, so lets think of another way.
What if we stored a list of multiples for each prime ? But that quickly becomes O(n^^2) - until we realize that we really only care about multiples that are “near” our current target number. In, fact we only care about a multiple if it is equal or larger than our target. And we really only care about the smallest such multiple. So, in fact, we only need one multiple per prime. And so, the Incremental Sieve.
In pseudo-code it wold look like:
for each number
for each prime
loop while current multiple < number
get next multiple of the prime
end
if number == current multiple
go to next number
end
end
add number to list of primes
end
Or in python:
#!/usr/bin/python3
primes = [ [2, 2] ]
for x in range(3, 100000) :
isprime = True;
for pm in primes :
while pm[1] < x :
pm[1] += pm[0]
if pm[1] == x :
isprime = False;
break
if isprime :
primes.append([x, x]);
print( len(primes))
This takes about 21 seconds to run on my ARM Chromebook. Not bad, but maybe we could do better.
One thing to notice is that all primes (above 2) will be odd. So, there is really no reason to iterate through the even numbers. But that means we don’t need to store the 2 either, since it will never do us any good.
#!/usr/bin/python3
primes = [ [3, 3] ]
for x in range(5, 100000, 2) :
isprime = True;
for pm in primes :
while pm[1] < x :
pm[1] += pm[0]
if pm[1] == x :
isprime = False;
break
if isprime :
primes.append([x, x]);
print( len(primes))
And that runs in about 20 secs. Hmmm. Not as helpful as I had hoped, but still worth it.
One fact to note about multiplication is that if a * b = c
then one of those
number must be below (or equal) to the square root of c and the other must be
above (or equal) to the square root. So, if c is not prime, then it must have
at least one of its factors below its square root. Which means, that if we
can’t find a factor there, we can stop looking. For our top value of 100,000,
we only need to search primes less than about 317. That is a huge savings.
#!/usr/bin/python3
import math
primes = [ [3, 3] ]
for x in range(5, 100000, 2) :
limit = int(math.sqrt(x))
isprime = True;
for pm in primes :
if pm[0] > limit :
break;
while pm[1] < x :
pm[1] += pm[0]
if pm[1] == x :
isprime = False;
break
if isprime :
primes.append([x, x]);
print( len(primes))
And this runs in about 0.6 seconds. Increasing the loop to 1,000,000, the progam takes about 9 seconds to run.
Just out of curiosity, I also implemented the last version in c++ :
#include <list>
#include <iostream>
#include <cmath>
using vtype = long long;
struct pm {
vtype prime;
vtype multiple;
pm(vtype p, vtype m) :prime(p), multiple(m) {}
};
std::list<pm> primes = { {3, 3} };
int main() {
for (vtype x = 5; x < 1'000'000; x+=2) {
vtype limit = std::sqrt(x) + 1;
bool isprime = true;
for (auto &[p, m] : primes) {
if (p > limit) break;
while (m < x) {
m += p;
}
if (m == x) {
isprime = false;
break;
}
}
if (isprime) {
primes.emplace_back(x, x*x);
}
}
std::cout << primes.size() << "\n";
}
Compiling and running we get :
$ g++ -std=c++17 -O3 incsieve.cpp
$ time ./a.out
78497
real 0m0.093s
user 0m0.084s
sys 0m0.010s
Well that is mighty nice. For the record, it took about 30 secs to run with the loop set to 100,000,000.
I hope you had fun looking at this variation on an old “favorite”.