Project Euler Problem 7

Jul 21st, 2022

11 min read

kotlinproject-euleralgorithms

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 nthn^{th} 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, nn, is prime is to divide it by every number between 2 and n1n-1. The function only returns true if none of the numbers evenly divide nn.

1fun Int.isPrime(): Boolean {
2 if (this < 2) return false
3 for (num in 2 until this) {
4 if (this % num == 0) return false
5 }
6 return true
7}
kt

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 n\lfloor\sqrt{n}\rfloor 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.sqrt
2
3fun Int.isPrime(): Boolean {
4 if (this < 2) return false
5 for (num in 2..sqrt(1.0 * this).toInt()) {
6 if (this % num == 0) return false
7 }
8 return true
9}
kt

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.sqrt
2
3fun Int.isPrime(): Boolean {
4 if (this < 2) return false
5 if (this % 2 == 0) return this == 2
6 for (num in 3..sqrt(1.0 * this).toInt() step 2) {
7 if (this % num == 0) return false
8 }
9 return true
10}
kt

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.sqrt
2
3fun Int.isPrime(): Boolean {
4 return when {
5 this < 2 -> false
6 this < 4 -> true
7 this % 2 == 0 || this % 3 == 0 -> false
8 else -> {
9 for (num in 5..sqrt(1.0 * this).toInt() step 2) {
10 if (this % num == 0) return false
11 }
12 true
13 }
14 }
15}
kt
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 * 3
2
3fun main() {
4 val num = 6
5
6 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}
kt
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() }
2
3fun main() {
4 val number = "1234"
5 val n = number.length
6 val k = 2
7
8 val result = when (n) {
9 1 -> number.toInt()
10 k -> sumOfDigits(number)
11 else -> {
12 var largest = 0
13 for (i in 0..n - k) {
14 largest = maxOf(largest, sumOfDigits(number.substring(i, i + k)))
15 }
16 largest
17 }
18 }
19
20 println(result) // 7
21}
kt

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 6k±16k\pm1

This is technically linked to fact #4, as any integer of the form 6k±16k\pm1, with k>0k>0, 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 6k16k-1 and performs an extra modulo operation to check the value of 6k+16k+1.

1import kotlin.math.sqrt
2
3fun Int.isPrime(): Boolean {
4 return when {
5 this < 2 -> false
6 this < 4 -> true
7 this % 2 == 0 -> false
8 this < 9 -> true
9 this % 3 == 0 -> false
10 else -> {
11 for (num in 5..sqrt(1.0 * this).toInt() step 6) {
12 if (this % num == 0 || this % (num + 2) == 0) return false
13 }
14 true
15 }
16 }
17}
kt

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.sqrt
2
3fun Int.isPrime(): Boolean {
4 return when {
5 this == 2 || this == 3 -> true
6 this < 2 || this % 2 == 0 || this % 3 == 0 -> false
7 else -> {
8 for (num in 5..sqrt(1.0 * this).toInt() step 6) {
9 if (this % num == 0 || this % (num + 2) == 0) return false
10 }
11 true
12 }
13 }
14}
kt

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: NAIVE7.93s
#2: SQRT1.39ms
#3: ODDS1.31ms
#4: MOD 35.1e+05ns
#5: FINAL2.7e+05ns

The Nth Prime

Now that we've created a helper function, it's just a matter of using it to find the nthn^{th} prime:

1fun getNthPrime(n: Int): Int {
2 var count = n
3 var number = 1
4 while (count > 0) {
5 if ((++number).isPrime()) count--
6 }
7 return number
8}
kt

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 n=10001n=10001) 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 == 0
2
3fun main() {
4 var number = 0
5 while (number < 5) {
6 if (!(++number).isEven()) println("${number++} is odd")
7 if (number++.isEven()) println("${--number} is even")
8 }
9}
kt
1 is odd
2 is even
3 is odd
4 is even
5 is odd
6 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.absoluteValue
2
3var number = 100
4val ans = number++.minus(150).absoluteValue
5println(ans) // 50
6println(number) // 101
kt

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 (n1)th(n-1)^{th} 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-1
3 */
4fun getAllPrimes(count: Int): List<Int> {
5 val primes = mutableListOf(2)
6 var number = 1
7 nextNum@while (primes.size < count) {
8 number += 2
9 for (p in primes) {
10 if (p * p > number) break
11 if (number % p == 0) continue@nextNum
12 }
13 primes.add(number)
14 }
15 return primes
16}
kt


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!🏝️🔎🍥

|© 2026 bog-walk