Here is an algorithm that is theoretically better than O(n log(n))
but may be worse for reasonable n
. I believe that its running time is O(n lg*(n))
where lg*
is the https://en.wikipedia.org/wiki/Iterated_logarithm.
First of all you can find all primes up to n
in time O(n)
using the Sieve of Atkin. See https://en.wikipedia.org/wiki/Sieve_of_Atkin for details.
Now the idea is that we will build up our list of counts only inserting each count once. We'll go through the prime factors one by one, and insert values for everything with that as the maximum prime number. However in order to do that we need a data structure with the following properties:
- We can store a value (specifically the count) at each value.
- We can walk the list of inserted values forwards and backwards in
O(1)
.
- We can find the last inserted number below
i
"efficiently".
- Insertion should be "efficient".
(Quotes are the parts that are hard to estimate.)
The first is trivial, each slot in our data structure needs a spot for the value. The second can be done with a doubly linked list. The third can be done with a clever variation on a skip-list. The fourth falls out from the first 3.
We can do this with an array of nodes (which do not start out initialized) with the following fields that look like a doubly linked list:
value
The answer we are looking for.
prev
The last previous value that we have an answer for.
next
The next value that we have an answer for.
Now if i
is in the list and j
is the next value, the skip-list trick will be that we will also fill in prev
for the first even after i
, the first divisible by 4, divisible by 8 and so on until we reach j
. So if i = 81
and j = 96
we would fill in prev
for 82, 84, 88
and then 96
.
Now suppose that we want to insert a value v
at k
between an existing i
and j
. How do we do it? I'll present pseudocode starting with only k
known then fill it out for i = 81
, j = 96
and k = 90
.
k.value := v
for temp in searching down from k for increasing factors of 2:
if temp has a value:
our_prev := temp
break
else if temp has a prev:
our_prev = temp.prev
break
our_next := our_prev.next
our_prev.next := k
k.next := our_next
our_next.prev := k
for temp in searching up from k for increasing factors of 2:
if j <= temp:
break
temp.prev = k
k.prev := our_prev
In our particular example we were willing to search downwards from 90
to 90, 88, 80, 64, 0
. But we actually get told that prev
is 81
when we get to 88
. We would be willing to search up to 90, 92, 96, 128, 256, ...
however we just have to set 92.prev
96.prev
and we are done.
Now this is a complicated bit of code, but its performance is O(log(k-i) + log(j-k) + 1)
. Which means that it starts off as O(log(n))
but gets better as more values get filled in.
So how do we initialize this data structure? Well we initialize an array of uninitialized values then set 1.value := 0
, 1.next := n+1
, and 2.prev := 4.prev := 8.prev := 16.prev := ... := 1
. And then we start processing our primes.
When we reach prime p
we start by searching for the previous inserted value below n/p
. Walking backwards from there we keep inserting values for x*p, x*p^2, ...
until we hit our limit. (The reason for backwards is that we do not want to try to insert, say, 18 once for 3 and once for 9. Going backwards prevents that.)
Now what is our running time? Finding the primes is O(n)
. Finding the initial inserts is also easily O(n/log(n))
operations of time O(log(n))
for another O(n)
. Now what about the inserts of all of the values? That is trivially O(n log(n))
but can we do better?
Well first all of the inserts to density 1/log(n)
filled in can be done in time O(n/log(n)) * O(log(n)) = O(n)
. And then all of the inserts to density 1/log(log(n))
can likewise be done in time O(n/log(log(n))) * O(log(log(n))) = O(n)
. And so on with increasing numbers of logs. The number of such factors that we get is O(lg*(n))
for the O(n lg*(n))
estimate that I gave.
I haven't shown that this estimate is as good as you can do, but I think that it is.
So, not O(n)
, but pretty darned close.