How to resolve the algorithm Legendre prime counting function step by step in the Ruby programming language
How to resolve the algorithm Legendre prime counting function step by step in the Ruby programming language
Table of Contents
Problem Statement
The prime-counting function π(n) computes the number of primes not greater than n. Legendre was the first mathematician to create a formula to compute π(n) based on the inclusion/exclusion principle.
To calculate:
Define
then
The Legendre formula still requires the use of a sieve to enumerate primes; however it's only required to sieve up to the √n, and for counting primes, the Legendre method is generally much faster than sieving up to n.
Calculate π(n) for values up to 1 billion. Show π(n) for n = 1, 10, 100, ... 109.
For this task, you may refer to a prime number sieve (such as the Sieve of Eratosthenes or the extensible sieve) in an external library to enumerate the primes required by the formula. Also note that it will be necessary to memoize the results of φ(x, a) in order to have reasonable performance, since the recurrence relation would otherwise take exponential time.
Comments on Task
Regarding "it will be necessary to memoize the results of φ(x, a)", it will have exponential time performance without memoization only if a very small optimization that should be obvious is not done: it should be obvious that one can stop "splitting" the φ(x, a) "tree" when 'x' is zero, and even before that since the real meaning of the "phi"/φ function is to produce a count of all of the values greater than zero (including one) up to x that have been culled of all multiples of the primes up to and including the pa prime value, if x is less than or equal to pa, then the whole "tree" must result in a value of just one. If this minor (and obvious) optimization is done, the "exponential time" performance goes away, memoization is not absolutely necessary (saving the overhead in time and space of doing the memoization), and the time complexity becomes O(n/(log n2) and the space complexity becomes O(n1/2/log n) as they should be.
This is the problem when non-mathematician programmers blindly apply such a general formula as the recursive Legendre one without doing any work to understand it or even doing some trivial hand calculations to better understand how it works just because they have very powerful computers which mask the limitations: a few minutes of hand calculation would make it obvious that there is no need to "split"/recursively call for "phi" nodes where the first argument is zero, and someone with a mathematics interest would then investigate to see if that limit can be pushed a little further as here to the range of nodes whose result will always be one. Once there is no exponential growth of the number of "nodes", then there is no need for memoization as usually implemented with a hash table at a huge cost of memory overhead and constant time computation per operation.
As to "the Legendre method is generally much faster than sieving up to n.", while the number of operations for the Legendre algorithm is about a factor of log n squared less than the number of operations for odds-only Sieve of Eratosthenes (SoE) sieving, those operations are "divide" operations which are generally much slower than the simple array access and addition operations used in sieving and the SoE can be optimized further using wheel factorization so that the required time for a given range can be less for a fully optimized SoE than for the common implementation of the Legendre algorithm; the trade off is that a fully optimized SoE is at least 500 lines of code, whereas the basic version of the Legendre prime counting algorithm is only about 40 to 50 lines of code (depending somewhat on the language used).
Also note that the Legendre prime counting function was never used practically at the time it was invented other than to demonstrate that it would find the count of primes to a trivial range only knowing the primes up to the square root of that range and there were too many operations (especially long integer division operations) to actually use it for any reasonably range even with this optimization (about 250 thousand divisions to count primes to ten million), but the follow-on work by Meissel in the 1800's definitely would have used this optimization and others in order to hand calculate the number of primes to a billion (1e9) in about ten years. Even with this optimization, Meissel would have had to hand calculate over five million divisions, so certainly used other Look Up Tables (LUT's) although certainly not caching of Phi/φ values in order to reduce the work to something possible in this amount of time. A "TinyPhi" LUT table for the first six primes of thirteen and less would have reduced the amount of work Meissel did to about 600 thousand divisions, but even that would have been perhaps too much and it is very likely that he also used "partial sieving" techniques, although that would have meant that as well as a table of the primes up to a million, he would have also needed 161 other tables of that range to a million sieved by the primes up to 13, 17, 19, to 997; however, that extra work in building these tables (which might have been done mechanically) would pay off in reducing the number of divisions to about seven thousand so the divisions become a minor problem possible to do over months and the majority of the time would be spent producing the partial sieving tables up to a million.
The reason that Meissel refined the Legendre method would have been that, even applying all of the optimizations including "partial sieving", he would still have had to do about three and a half million divisions to count the primes to a billion even if the number of primes and "partial sieve tables" only needed to be known to about 32 thousand, where his "Meissel" algorithm reduced the number of divisions to only a few thousand as per the above. Without a computer, he could never have completed the calculation of the number of primes to a billion using an optimized Legendre algorithm where he could using his modification. However, modern computers make (reasonably) quick work of integer divisions so that optimized algorithms of the Legendre type become moderately useful although at the cost of memory use as compared to Meissel type algorithms.
Let's start with the solution:
Step by Step solution about How to resolve the algorithm Legendre prime counting function step by step in the Ruby programming language
The provided Ruby code is designed to calculate the prime counting function, denoted as "pi(n)", which counts the number of prime numbers less than or equal to a given positive integer n. Here's a step-by-step explanation of how the code works:
-
Importing the 'prime' Library:
- The code begins by requiring the 'prime' library, which provides functions for working with prime numbers.
-
pi(n) Method:
- This method is defined to calculate the prime counting function for a given integer n.
- It calculates the prime numbers up to the square root of n using the Prime.each method from the 'prime' library and stores them in the instance variable @pr.
- Based on the value of n, it then determines the count of primes using the phi method.
-
phi(x, a) Method:
- This method recursively calculates the value of phi(x, a), which is used in the pi(n) method to count the prime numbers less than or equal to x.
- It takes two parameters: x (the number for which we want to count primes) and a (the index of the prime number in the @pr array).
- Depending on the value of a, it calculates phi(x, a) using various cases:
- If a is 0, it returns x directly.
- If a is 1, it returns x minus half of x (x-(x>>1)), which is used as a special case optimization.
- If a is greater than 1, it calculates phi(x, a) using recursion. It finds the next prime number (pa) in the @pr array and subtracts the count of primes less than or equal to x/pa (phi(x/pa, a-1)) from the count of primes less than or equal to x (phi(x, a-1)).
-
Main Calculation:
- The code then iterates over the range 0 to 9 and calculates the prime counting function for each power of 10, i.e., from 10^0 to 10^9.
- For each value of n, it prints the result in the form "10E#{n} #{pi(10**n)}".
-
Output:
- The output of the code is a table that shows the number of primes up to various powers of 10, such as:
| 10E0 | 4 | | 10E1 | 25 | | 10E2 | 168 | | 10E3 | 1229 | | 10E4 | 9592 | | 10E5 | 78498 | | 10E6 | 664579 | | 10E7 | 5761455 | | 10E8 | 50847534 | | 10E9 | 455052511 |
This code effectively calculates and demonstrates the prime counting function for various large integers using a combination of prime factorization and recursive techniques.
Source code in the ruby programming language
require 'prime'
def pi(n)
@pr = Prime.each(Integer.sqrt(n)).to_a
a = @pr.size
case n
when 0,1 then 0
when 2 then 1
else phi(n,a) + a - 1
end
end
def phi(x,a)
case a
when 0 then x
when 1 then x-(x>>1)
else
pa = @pr[a-1]
return 1 if x <= pa
phi(x, a-1)- phi(x/pa, a-1)
end
end
(0..9).each {|n| puts "10E#{n} #{pi(10**n)}" }
You may also check:How to resolve the algorithm Ordered partitions step by step in the Ruby programming language
You may also check:How to resolve the algorithm Holidays related to Easter step by step in the Ruby programming language
You may also check:How to resolve the algorithm Inverted index step by step in the Ruby programming language
You may also check:How to resolve the algorithm Narcissistic decimal number step by step in the Ruby programming language
You may also check:How to resolve the algorithm MD5 step by step in the Ruby programming language