Smallest Multiple
Project Euler presents the following Problem #5:
2520 is the smallest number that can be divided by each of the numbers from 1 to 10 without any remainder.
What is the smallest positive number that is evenly divisible by all of the numbers from 1 to 20?
The smallest positive number that can be evenly divided by a range of numbers is the least common multiple (LCM) of that range of numbers. We got a simplified introduction to calculating the LCM in Project Euler Problem 1, so let's cover a few more ways.
LCM Formula - Long
The LCM of two numbers can be calculated using the greatest common divisor (GCD):
In the following function we'll use Long values to avoid any overflow and use a vararg parameter so a range of numbers can be provided.
1import kotlin.math.abs23fun lcm(vararg n: Long): Long {4 require(n.all { it != 0L }) { "Argument cannot be 0" }5 return n.reduce { acc, num ->6 abs(acc * num) / gcd(acc, num)7 }8}910/**11 * Calculates the GCD of n1 and n2 using the Euclidean algorithm and recursion,12 * based on the formula:13 *14 * gcd(x, y) = gcd(|y % x|, |x|); where |y| >= |x|15 *16 * gcd(x, 0) = gcd(0, x) = |x|17 */18fun gcd(n1: Long, n2: Long): Long {19 val x = abs(n1)20 val y = abs(n2)21 if (x == 0L || y == 0L) return x + y22 val bigger = maxOf(x, y)23 val smaller = minOf(x, y)24 return gcd(bigger % smaller, smaller)25}
Line 4: The number 0 is excluded for two reasons. Firstly, the division of integers by zero is undefined so finding a common number evenly divisible by 0 is also undefined. Secondly, if one of the arguments was 0, the only common multiple of 0 with other numbers would also be 0. For this reason, require() could be replaced with a conditional return statement that returns 0 if any of the arguments equal 0 or simply returning the result of reduce().
Line 5: reduce() is an aggregate function that accumulates a value (in this case, the cumulative LCM for every pair of numbers), thereby simplifying the written code. If reduce() was not used, a var would have to be declared to store each iteration of the LCM.
When a vararg function is called, the arguments can be passed either individually as lcm(1,2,3) or as an array. For this problem we want to pass the range 1..20 but calling lcm(1..20) would cause a compile error:
Type mismatch: inferred type is IntRange but Long was expected
Converting the range to a LongRange would not fix the error. Instead, it has to be converted to a List then to a LongArray, which can then be passed using the spread operator.
Note that it is more efficient to use a more narrow range that steps backwards from the largest value to the middle value, as the smaller half of the range will already be factors of the larger half.
1// the range in parentheses is technically a LongProgression type2val range = (20L downTo 20L / 2 + 1L).toList().toLongArray()3val answer = lcm(*range)
LongRange vs LongProgression
The distinction to remember is that the LongProgression class implements the Iterable interface and the LongRange class extends LongProgression (i.e. LongRange inherits the properties and functions of the latter).
Here's a list of different ways to initialize a range and their corresponding instance types:
1val iterables = listOf(2 1L..20L,3 1L.rangeTo(20L),4 1L until 20L,5 (1L..20L).reversed(),6 20L downTo 1L,7 1L..20L step 28)9iterables.forEach {10 println("$it is a ${it::class.simpleName}")11}
1..20 is a LongRange1..20 is a LongRange1..19 is a LongRange20 downTo 1 step 1 is a LongProgression20 downTo 1 step 1 is a LongProgression1..19 step 2 is a LongProgression
It seems that Kotlin's type inference will consider a range as a LongProgression if either the start value is greater than the end value or a step has been explicitly defined.
Following the inheritance rules, a LongRange (subclass) can be upcast to a LongProgression (superclass) without any errors, either implicitly or explicitly. The upcast instance will of course not be able to access properties/functions specific to LongRange, for example LongRange.endInclusive.
Conversely, downcasting a LongProgression requires an explicit cast to avoid a Type mismatch error.
1// implicit upcast2val rToP1: LongProgression = 1L..20L3// explicit upcast4val rToP2 = (1L..20L) as LongProgression5// explicit downcast6val pToR: LongRange = (20L downTo 1L step 4) as LongRange
The same patterns for ranges and progressions apply to the other integral types, such as Int and Char.
This solution uses vararg because it is adapted to take a variable amount of arguments for future use, not necessarily just a range. If you wanted to scale the solution function while keeping it specific to this problem, you could limit the function argument to only the upper range limit:
1fun lcmOfRange(limit: Long): Long {2 return if (limit > 0) {3 (1L..limit).reduce { acc, num ->4 abs(acc * num) / gcd(acc, num)5 }6 } else 07}
This post could be concluded right here, but I've had to use lcm() in a number of other Project Euler problems and at some point along the way I started to notice how it underperformed with larger numbers compared to an alternative that I was testing, which used BigInteger built-in methods.
LCM Formula - BigInteger
Normally, I attempt to avoid using BigInteger unless the problem space calls for it, which does happen occasionally when scaling Project Euler solutions to fit more challenging upper constraints. This solution does not provide an advantage for Problem #5 as the resulting LCM is only nine digits long, but its performance becomes more interesting when function arguments start resembling, for example, a range of 3-digit integers that has an 18-digit LCM.
The formula is the same and reduce() is still used, but all arguments are first converted to BigInteger before leveraging the built-in methods via an extension function.
1import java.math.BigInteger23fun lcmUsingBI(vararg n: Long): Long {4 require(n.all { it != 0L }) { "Argument cannot be 0" }5 val nBI = n.map(BigInteger::valueOf)6 return nBI.reduce { acc, num -> acc.lcm(num) }.longValueExact()7}89fun BigInteger.lcm(other: BigInteger): BigInteger {10 return (this * other).abs() / this.gcd(other)11}
The BigInteger class has gcd() with optimum performance, which
uses MutableBigInteger instances and a hybrid function that
initially implements the Euclidean algorithm then switches to a binary gcd algorithm
at smaller values.
LCM - Prime Numbers
The Sieve of Eratosthenes has already been introduced to implement a prime number generator in Project Euler Problem 3. We can use this to get all prime numbers less than or equal to the upper range limit (), so that we can then determine the exponent () of each prime () based on the formula:
For example, if the desired range is 1..6, the prime numbers are .
The exponent of the first prime, 2, will be 2 as but .
The exponent of the second prime, 3, will be 1 as but .
The exponent of the third prime, 5, will be 1 as but .
The LCM can then be calculated as .
1import kotlin.math.log22import kotlin.math.pow3import kotlin.math.sqrt45fun lcmUsingPrimes(n: Int): Long {6 var lcm = 1L7 val primes = primeNumbers(n)8 for (prime in primes) {9 val exponent = if (prime * prime <= n) {10 (log2(1.0 * n) / log2(1.0 * prime)).toInt()11 } else 112 lcm *= ((1.0 * prime).pow(exponent)).toLong()13 }14 return lcm15}1617fun primeNumbers(n: Int): List<Int> {18 if (n < 2) return emptyList()19 val oddSieve = (n - 1) / 220 val boolMask = BooleanArray(oddSieve + 1) { true }21 for (i in 1..(sqrt(1.0 * n).toInt() / 2)) {22 if (boolMask[i]) {23 var j = 2 * i * (i + 1)24 while (j <= oddSieve) {25 boolMask[j] = false26 j += 2 * i + 127 }28 }29 }30 return boolMask.mapIndexed { i, isPrime ->31 if (i == 0) 2 else if (isPrime) 2 * i + 1 else null32 }.filterNotNull()33}
This is a more concise adaptation of the prime factorization method that returns the same result by generating the prime factor representation of each number in the range and using the maximum exponent of each prime to perform the same calculation, based on the formula:
For example, if the desired range is 1..6 (remember that 1 cannot undergo prime factorization):
If you're interested in altering this solution to use prime factorization, checkout Project Euler Problem 3.
Speed Comparison
I ran a benchmark over 1000 iterations on my system and got the following results for two different ranges:
| RANGE 1..20 | |||
|---|---|---|---|
| SOLUTION | BEST (ns) | AVERAGE (ns) | WORST (ms) |
| LONG | 9800 | 4.2e+04 | 8.92 |
| BIGINTEGER | 1.4e+04 | 5.2e+04 | 1.74 |
| PRIMES | 1.1e+04 | 2.2e+04 | 5.60 |
| RANGE 1..40 | |||
| SOLUTION | BEST (ns) | AVERAGE (ns) | WORST (ms) |
| LONG | 2.1e+04 | 4.6e+04 | 10.69 |
| BIGINTEGER | 3.0e+04 | 1.2e+05 | 2.02 |
| PRIMES | 1.8e+04 | 3.0e+04 | 6.78 |
The performance of the prime number solution was a surprise, but it really is just iterating over a very small amount of generated primes and performing some calculations on them. In fact, for the range 1..20, only eight primes are generated, of which only the first two need log calculations. Given that the majority of primes would have an exponent of 1, the need to cast and use pow() for all primes could be minimized by including the conditional block in the augmented assignment block:
1fun lcmUsingPrimes(n: Int): Long {2 // ✂️3 for (prime in primes) {4 lcm *= if (prime * prime <= n) {5 val exponent = (log2(1.0 * n) / log2(1.0 * prime)).toInt()6 ((1.0 * prime).pow(exponent)).toLong()7 } else {8 prime.toLong()9 }10 }11 return lcm12}
Running this updated version of the prime number solution through the benchmarks resulted in reduced execution times:
| RANGE | BEST (ns) | AVERAGE (ns) | WORST (ms) |
|---|---|---|---|
| 1..20 | 3100 | 1.2e+04 | 1.93 |
| 1..40 | 7300 | 1.9e+04 | 3.82 |
Python Solution
If you're solving this problem using Python, any of the previous solutions can be converted and combined with reduce() from Python's functools library to iterate over multiple arguments.
1from functools import reduce23def lcm(x, y):4 # manual implementation5 # ✂️67n = 208ans = reduce(lcm, range(n, n // 2, -1))
But since version 3.9 released, the math library comes with lcm(*integers) that does everything we need. All we have to do is optimize the range passed as an argument. 🍰
1# Since PY 3.92from math import lcm34n = 205ans = lcm(*range(n, n // 2, -1))
Visit the following links to see the Kotlin and Python source code and test suites in their respective repositories:
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!🐍💭🏝️