A prime number (or a prime) is a natural number greater than 1 that is not a product of two smaller natural numbers - Wikipedia Page of 'Prime Numbers'
In essence, a prime number is only divisible by 1 and itself. Identifying prime numbers efficiently is a classic problem in computer science and mathematics. Here, we'll first explore some simple methods to determine if a number is prime.
The simplest way to check if a number is prime is to test by dividing with all smaller numbers.
def is_prime_naive(n: int) -> bool:
if n <= 2:
return True
for i in range(2, n): #Checking all numbers from 2..n
if n%i == 0:
return False
else:
i += 1
return True
The method is fairly straightforward, but for every number, in the worst case has to perform n-1 checks, effectively a
We can improve the previous result by checking divisors up to the square root of n.
def is_prime_reduced_range(n: int) -> bool:
if n <= 2:
return True
i = 2
while i * i <= n: # Effectively putting range from 2..root(n)
if n%i == 0:
return False
else:
i += 1
return True
Why? Because we see that if
We can take this quite further by using a simple observation.
Every number
Where
Now we can say for certain that
So let's take
Out of these we can clearly see:
and have as their factor has as its factor
Thus we are left with
Now we can use this property to decrease our search time even more, by only checking divisibility with number which are less that
def is_prime_reduced_range_2(n: int) -> bool:
if n <= 2:
return True
if n%2 == 0 or n%3 == 0: # Checking divisibility by 2 and 3
return False
i = 6
while i * i <= n: # Effective range is still 2..root(n)
# Checking only those numbers,
# which are of the form 6k + 1 and 6k + 5
#
# For simplicty i here,
# is changed to 6k+1 and 6k-1.
# This can be implemented by offsetting k
if n%(i-1) == 0 or n%(i+1) == 0:
return False
else:
i += 6
return True
Here the complexity remains
Now, we can compare the performances of the program.
On running this program on input 9737333 we see the following results:
| Benchmark | Runtime | Operations[1] | VS[2] |
|---|---|---|---|
is_prime_reduced_range_2 |
106 us | 9.40K op/s | 1× |
is_prime_reduced_range |
640 us | 1.56K op/s | 6× |
is_prime_naive |
1.87 s | 0.535 op/s | 17577× |
Our last two methods are quite on par, with the difference clearly being
And as our naive method did a lot of unnecessary calculations, it has been left in the dust.
As numbers become larger, our simple methods, although quite efficient, cannot keep up. In this case, we resort to probabilistic tests, with our main trade-off being speed for accuracy. These tests provide a probabilistic guarantee of primality, which means they can identify composite numbers with certainty but may have a small chance of misidentifying a composite number as prime.
The Fermat Primality Test is based on Fermat's Little Theorem, which states that for a number
If
is anything other than 1, we can classify as a composite number. But, if , then may or may not be a prime.
This test is relatively simple to implement using modular exponentiation, as discussed in The Trick of Exponentiation > Modular Exponentiation.
Here is a Python implementation of the Fermat Primality Test:
import random
def fermat_primality_test(n, k=5):
# Base cases
if n <= 1:
return False
if n <= 3:
return True
# Perform the test k times
for _ in range(k):
# Pick a random integer in the range [2, n-2]
a = random.randint(2, n - 2)
# Compute a^(n-1) % n
if pow(a, n - 1, n) != 1:
return False
return True
# Example usage
n = 97
print(fermat_primality_test(n)) # Output: True or False (with high probability)
The Miller-Rabin Primality Test is a more sophisticated probabilistic test that provides a stronger guarantee than the Fermat Primality Test. It is based on the properties of strong pseudo-primes and is widely used in practice due to its reliability.
The test involves the following steps:
Here is a Python implementation of the Miller-Rabin Primality Test:
import random
def miller_rabin_primality_test(n, k=5):
# Base cases
if n <= 1:
return False
if n <= 3:
return True
if n % 2 == 0:
return False
# Write n-1 as 2^s * d with d being odd
s, d = 0, n - 1
while d % 2 == 0:
s += 1
d //= 2
# Perform the test k times
for _ in range(k):
a = random.randint(2, n - 2)
x = pow(a, d, n)
if x == 1 or x == n - 1:
continue
for _ in range(s - 1):
x = pow(x, 2, n)
if x == n - 1:
break
else:
return False
return True
# Example usage
n = 97
print(miller_rabin_primality_test(n)) # Output: True or False (with high probability)
[3]
The prime-counting function is a function that counts number of prime numbers
We can use this property to get a probabilistic guess of whether a number is prime or not.
Furthermore we can decide our probability as
Here if we consider our range to be
Now we can calculate the probability for a few numbers and see the results
Due to the nature of log, the function deteriorates slowly, this is good, but can we do better?
In 1899, de la Vallée Poussin proved:
Here the important part is the
We can create a similar table to see how this function behaves,
We see higher probabilities for the numbers, which can be attributed to the lower error of
But can we push this further?
In 2015 Dudek (2015) proved that the Riemann hypothesis implies that for all x ≥ 2 there is a prime p satisfying:
which can be considered a tighter formula relative to Betrand's postulate which states there exists one prime number
We will go ahead with Dudek as it give a shorter interval. Now as we have an inequality for simplicity we can assume
Simply to find the probability of a number
Now using our
where
Now we can define our
And for
Now all that is use these probabilities, and toss a coin, hoping to get the correct answer.
That was quite a lot of prime testing methods. Due to their unpredictable distribution, we can as of now at best use heuristics or do rigorous testing to check primality. Perhaps in the future, we could have a simple