The 10001st Prime
Project Euler presents the following Problem #7:
By listing the first six prime numbers: 2, 3, 5, 7, 11, and 13, we can see that the 6th prime is 13.
What is the 10 001st prime number?
The Sieve of Eratosthenes algorithm has already been introduced to implement a prime number generator in Project Euler Problem 3. For this problem, since we don't know the upper limit necessary to generate the prime number in question (or do we?), we'll iterate over increasing numbers and perform a primality test on each until a counter determines that the prime has been found.
Primality Test
There are quite a few possible approaches to writing a primality test, either deterministic or probabilistic. Future problems will require probabilistic algorithms like the Rabin-Miller test, but we don't have to worry about checking extremely large numbers in this problem. For now, we can safely use the simplest trial division method, along with some optimizations based on what we know about prime numbers.
Fact 1: The smallest prime is 2
The most naive way to check whether a number, , is prime is to divide it by every number between 2 and . The function only returns true if none of the numbers evenly divide .
1fun Int.isPrime(): Boolean {2 if (this < 2) return false3 for (num in 2 until this) {4 if (this % num == 0) return false5 }6 return true7}
Fact 2: A prime can only have 1 factor (itself) greater than its square root
As already explained in Problem 3, iterating over numbers greater than will cause unnecessary checks, as the cofactors of any larger factors will have already been tested. The only distinct factor greater than this upper limit would be the prime number itself.
1import kotlin.math.sqrt23fun Int.isPrime(): Boolean {4 if (this < 2) return false5 for (num in 2..sqrt(1.0 * this).toInt()) {6 if (this % num == 0) return false7 }8 return true9}
Fact 3: All primes are odd except 2
We can tighten our loop even further by removing the only even prime from the iteration and stepping through odd numbers only. This means a conditional branch has to be added to ensure that all even numbers are caught and properly processed, to avoid them either incorrectly entering or bypassing the loop.
1import kotlin.math.sqrt23fun Int.isPrime(): Boolean {4 if (this < 2) return false5 if (this % 2 == 0) return this == 26 for (num in 3..sqrt(1.0 * this).toInt() step 2) {7 if (this % num == 0) return false8 }9 return true10}
Fact 4: As 3 is a prime, no multiple of 3 can be a prime
Let's rewrite the function to use a when block and incorporate new conditional branches to catch prime numbers 2 and 3, as well as any composites that are multiples of them. The for loop's lower limit is also increased to 5, as it is the lowest prime that is not assessed by previous conditional branches.
1import kotlin.math.sqrt23fun Int.isPrime(): Boolean {4 return when {5 this < 2 -> false6 this < 4 -> true7 this % 2 == 0 || this % 3 == 0 -> false8 else -> {9 for (num in 5..sqrt(1.0 * this).toInt() step 2) {10 if (this % num == 0) return false11 }12 true13 }14 }15}
When Blocks
Kotlin's when block is similar to Java's switch statement with a few key differences.
The provided argument is matched against each branch sequentially until a matching condition is found, at which point the respective case block is executed and the when block is exited. This means that no break statements are required to avoid fall through to the next case.
In the following example, the argument num matches all branches except the first one, but only the first matching branch will execute, while all the rest will be ignored.
1fun doCalc() = 2 * 323fun main() {4 val num = 656 when (num) {7 0, 3, 9 -> println("Wrong value")8 6 -> println("Value is equivalent")9 in 0..10 -> println("Value in range")10 6.0.toInt() -> println("Value matches Double")11 doCalc() -> println("Value matches function")12 }13}
Value is equivalent
when blocks can also be used as expressions, either returning a value to be assigned to a variable or to be used with return. The value of the overall expression will be the value of the last expression in the case block. When used this way, the else branch is mandatory unless the branches cover all possible cases, for example, with an enum class.
1fun sumOfDigits(digits: String) = digits.sumOf { it.digitToInt() }23fun main() {4 val number = "1234"5 val n = number.length6 val k = 278 val result = when (n) {9 1 -> number.toInt()10 k -> sumOfDigits(number)11 else -> {12 var largest = 013 for (i in 0..n - k) {14 largest = maxOf(largest, sumOfDigits(number.substring(i, i + k)))15 }16 largest17 }18 }1920 println(result) // 721}
As shown in the function Int.isPrime(), the only way to use general boolean expressions as branch conditions is to omit providing an argument to the when block. The first branch with an expression that evaluates to true will be the only branch that executes, regardless of whether subsequent branches also evaluate to true.
Fact 5: All primes greater than 3 are of the form
This is technically linked to fact #4, as any integer of the form , with , cannot be a multiple of 3. So, rather than iterating over odd numbers, the final form of our helper function steps through only numbers of the form and performs an extra modulo operation to check the value of .
1import kotlin.math.sqrt23fun Int.isPrime(): Boolean {4 return when {5 this < 2 -> false6 this < 4 -> true7 this % 2 == 0 -> false8 this < 9 -> true9 this % 3 == 0 -> false10 else -> {11 for (num in 5..sqrt(1.0 * this).toInt() step 6) {12 if (this % num == 0 || this % (num + 2) == 0) return false13 }14 true15 }16 }17}
Line 8: This conditional branch catches prime numbers 5 and 7, but isn't strictly necessary as they would both correctly bypass the for loop on line 11 and return true.
Line 11: The trial division loop still only iterates through odd numbers (remember that an odd number plus an even number can only produce an odd number) and skips all multiples of 3, so both original conditionals (line 7 and line 9) must remain. Note that the primes 11, 13, 17, 19, and 23 will correctly bypass this loop due to their square roots being less than 5. This means that the first integer to enter this loop will be 25.
This final version is the top-level helper function that I use whenever invoking Int values are not expected to exceed Int.MAX_VALUE. Personally, I prefer to wrap the sole purpose of the function into a single return statement with a sequential chain of conditional branches, but the conditionals could, of course, be grouped together by output (or removed from the when block entirely and replaced with two if statements and a for loop):
1import kotlin.math.sqrt23fun Int.isPrime(): Boolean {4 return when {5 this == 2 || this == 3 -> true6 this < 2 || this % 2 == 0 || this % 3 == 0 -> false7 else -> {8 for (num in 5..sqrt(1.0 * this).toInt() step 6) {9 if (this % num == 0 || this % (num + 2) == 0) return false10 }11 true12 }13 }14}
Speed Comparison
I ran a benchmark test on the 5 versions of Int.isPrime() over 1000 iterations using Int.MAX_VALUE as the calling value. Here's the average time each version took to determine that 2_147_483_647 is a prime number:
| isPrime() | AVERAGE |
|---|---|
| #1: NAIVE | 7.93s |
| #2: SQRT | 1.39ms |
| #3: ODDS | 1.31ms |
| #4: MOD 3 | 5.1e+05ns |
| #5: FINAL | 2.7e+05ns |
The Nth Prime
Now that we've created a helper function, it's just a matter of using it to find the prime:
1fun getNthPrime(n: Int): Int {2 var count = n3 var number = 14 while (count > 0) {5 if ((++number).isPrime()) count--6 }7 return number8}
We could, of course, include some optimizations in this function, for example, by only invoking the helper function with odd values. On my system, leaving the optimization to the helper function actually made getNthPrime() execute at an average of 7.80ms (for ) compared to 8.21ms when the function was re-written to increment number by 2 instead of 1.
Postfix vs Prefix Operators
The increment (and decrement) operator is a convenient tool used to reduce code. The increment operator on line 5 of getNthPrime() replaces an extra preceding line: number++ or number += 1.
The key to remembering the result of a postfix versus a prefix increment operator is to remember the order of operations, which I've covered in a previous post.
Postfix operators have highest precedence, so ++number.isPrime() would not compile as number.isPrime() would be evaluated first and a Boolean returned, causing a receiver type mismatch error when the prefix operator is evaluated. This is why parentheses have to be used to break the natural order.
These operators can become confusing if used inappropriately or too often. Look no further than this purposefully convoluted example:
1fun Int.isEven() = this % 2 == 023fun main() {4 var number = 05 while (number < 5) {6 if (!(++number).isEven()) println("${number++} is odd")7 if (number++.isEven()) println("${--number} is even")8 }9}
1 is odd2 is even3 is odd4 is even5 is odd6 is even
On the first iteration, when line 7 is reached, number equals 2 and this value is used to call isEven() before number is incremented to 3 (which is why it has to be decremented inside println() to get the correct output). number++.isEven() does not require parentheses because ++ only increments the variable after the expression has been evaluated using the original value.
Here's a much smaller example to show that the postfix increment operator plays no role in the returned value of chained functions, only incrementing the original variable after the rest of the expression has been evaluated.
1import kotlin.math.absoluteValue23var number = 1004val ans = number++.minus(150).absoluteValue5println(ans) // 506println(number) // 101
If this were not the case, one might incorrectly assume that ans would be evaluated as either 49 or 51. To emphasize this further, if I remove the final line 6, my IDE actually flags a warning on line 4 that states: The value changed at 'number++' is never used.
Alternative Solution
At the beginning of this post I stated that we didn't know the upper limit for our prime number generator. However, if you are also using the sieve algorithm, it's honestly fast enough that you could have played around with a few numbers to get an approximate idea of the size of the generated lists. A prime counting function also exists, as do online calculators that would quickly determine an appropriate upper limit for this problem.
In fact, if the aim was to create a list of primes for quick-draw access to the prime, and you didn't want to either guess or brute force the upper limit, there is an alternative. This list could have been simultaneously generated and used as a primality test based on prime factorization. For every odd number checked, that number would be a prime if none of the previously found primes were able to evenly divide it or if a prime was reached that exceeded the number's square root. If found to be a prime, the number would be added to the list and the loop continued until the size of the generated list reached the desired maximum.
1/**2 * @return list of [count] primes with the nth prime at index n-13 */4fun getAllPrimes(count: Int): List<Int> {5 val primes = mutableListOf(2)6 var number = 17 nextNum@while (primes.size < count) {8 number += 29 for (p in primes) {10 if (p * p > number) break11 if (number % p == 0) continue@nextNum12 }13 primes.add(number)14 }15 return primes16}
Here's the full source code for this problem (and its corresponding test suite).
This solution set is also available in Python and in C++ on my GitHub.
Problem content on the Project Euler site is licensed under a Creative Commons (CC) License: CC BY-NC-SA 4.0. Solutions to only the first hundred problems are encouraged to be distributed publicly.
Thanks for reading!🏝️🔎🍥