Largest Product in a Series
Project Euler presents the following Problem #8:
The four adjacent digits in the 1000-digit number that have the greatest product are .
Find the thirteen adjacent digits in the 1000-digit number that have the greatest product. What is the value of this product?
Product of String Digits
Before solving the larger problem at hand, let's decide how to approach the smaller task of calculating the product of all digits in a String
of unknown size.
The obvious answer is to use a simple for
loop that iterates over all characters and converts them to their numeric value:
1fun stringProduct(series: String): Long {2 var product = 1L3 for (ch in series) {4 product *= ch - '0'5 }6 return product7}
Line 1: If the length of the String
is not expected to exceed thirteen characters, then the resulting product will not overflow Long
, as does not exceed Long.MAX_VALUE
.
Casting Char to Int
There are a couple of ways to convert a Char
to its numeric value, one of which is shown in stringProduct()
. Additive operations can be performed on two Char
values, so subtracting '0'
from a Char
produces an Int
value equivalent to the character's numeric representation.
This is handy when we know the expected range of our character inputs, as in this case when we should only be seeing characters between '0' and '9' inclusive.
A similar method uses Char.code
, which returns the UTF-16 code unit associated with the Char
. While Char.toInt()
and Char.toLong()
have been deprecated, Char.code
returns an Int
from which the corresponding unit for '0' can be subtracted to get the same result: ch.code - 48
.
If we didn't have a baseline, or we were dealing with a wider range of characters, we could also borrow from Java and use Character.getNumericValue(ch)
.
Kotlin version 1.5 introduced Char.digitToInt()
, which neatly returns the decimal digit value of a provided digit character (with an optional radix argument that defaults to base 10). This will be used in the solutions to follow.
This function works just fine, but let's use the tools that Kotlin gives us.
First, we'll use a built-in extension function, digitToInt()
that converts a Char
to its decimal equivalent.
Second, whenever a collection of elements needs to be transformed into a single element by performing the same accumulative operation repeatedly, we should consider using some sort of aggregate function, in this case either reduce()
or fold()
(or one of their respective variants). fold()
is preferable in this case because it allows the return value to be of a different type than the elements in the collection, based on the type of the initial value passed as an argument:
1fun stringProduct(series: String): Long {2 return series.fold(1L) { acc, ch ->3 acc * ch.digitToInt()4 }5}
There is a slight inefficiency in this function that becomes apparent when arguments that contain the character '0' are passed. Let's add a println()
to see what happens when we provide "23034"
as an argument:
1fun stringProduct(series: String): Long {2 return series.fold(1L) { acc, ch ->3 println("Product = ${acc * ch.digitToInt()}")4 acc * ch.digitToInt()5 }6}78println(stringProduct("23034"))
Product = 2Product = 6Product = 0Product = 0Product = 00
Given that any digit multiplied by 0 would return 0 regardless of either the position of '0' in the String
or what characters come after, we should probably include an early termination for this event.
The joy of Kotlin's trailing lambda expressions is that so much can be done in their temporary scope, with only the value of the final expression being implicitly returned. So, let's add a conditional block that causes a premature exit from the operation as soon as the character '0' is encountered:
1fun stringProduct(series: String): Long {2 return series.fold(1L) { acc, ch ->3 if (ch == '0') {4 println("'0' encountered")5 return 0L6 } else {7 println("Product = ${acc * ch.digitToInt()}")8 acc * ch.digitToInt()9 }10 }11}1213println(stringProduct("23034"))
Product = 2Product = 6'0' encountered0
Rather than iterating indiscriminately over every character, fold()
now terminates if the character '0' is found, thereby returning a value sooner. This is the final form of the helper function that will be used by our solutions (minus the print statements, of course).
Return From a Lambda
The return
expression on line 5 of stringProduct()
forces a non-local return from the nearest enclosing function, stringProduct()
itself, as shown in the following modified version:
1fun stringProduct(series: String): Long {2 val product = series.fold(1L) { acc, ch ->3 if (ch == '0') {4 return 0L5 } else {6 acc * ch.digitToInt()7 }8 }9 println("This will not be printed")10 return product11}1213fun main() {14 val series = "23034"15 println("String product of $series = ${stringProduct(series)}")16}
String product of 23034 = 0
If a local return from the lambda expression is needed instead, use an implicit label to achieve this (a named or qualified label also works):
1fun stringProduct(series: String): Long {2 val product = series.fold(1L) { acc, ch ->3 if (ch == '0') {4 return@fold 0L5 } else {6 acc * ch.digitToInt()7 }8 }9 println("Perform some extra work")10 return if (product == 0L) -1L else product11}1213fun main() {14 val series = "23034"15 println("String product of $series = ${stringProduct(series)}")16}
Perform some extra workString product of 23034 = -1
Python Helper
As a comparison, Python also has aggregate functions, with the math
module in version 3.8 introducing prod()
that calculates the product of elements in an iterable. reduce()
is also available if we wanted to pass a custom lambda. Neither allow the operation to be terminated early, however.
1from math import prod2from functools import reduce34def string_product(string: str) -> int:5 return prod(map(int, string))67def string_product_custom(string: str) -> int:8 return reduce(lambda acc, ch: (acc * int(ch)), string, 1)
Iterative Solution
To find the answer to Problem #8, we need to continuously slide across the input String
and check the product of every -digit substring, with being the requested number of adjacent digits.
Kotlin easily enables this with either windowed()
or windowedSequence()
(more about these extension functions); however, windowed()
generates a List
of all relevant snapshots for processing.
1fun largestSeriesProductWindowed(number: String, k: Int): Long {2 return number.windowed(k) { series ->3 stringProduct(series.toString())4 }.maxOf { it }5}
In the case of a thousand-digit String
, with , this means the temporary storage of 988 elements in memory when the only value of interest to us is the largest product found so far. Using a Sequence
instead seems a wasted attempt at improving performance in this situation as the result of the whole processing chain will need to be ultimately requested in order to get an accurate final answer.
Sticking with a simple for
loop seems the best choice for this problem. There are also certain conditions to check for that would warrant avoiding the loop entirely:
- If the provided
String
has only one character, it just needs to be cast. - If then only the largest character needs to be found and cast.
- If is equal to the
String
size , the helper function only needs to be called once.
1fun largestSeriesProduct(number: String, n: Int, k: Int): Long {2 return when {3 n == 1 -> number.toLong()4 k == 1 -> number.maxOf { it.digitToInt() }.toLong()5 n == k -> stringProduct(number)6 else -> {7 var largest = 0L8 for (i in 0..n - k) {9 largest = maxOf(largest, stringProduct(number.substring(i, i + k)))10 }11 largest12 }13 }14}
Recursive Solution
For curiosity's sake, let's tweak our solution to make it recursive, since the problem could be approached by performing the same operation on an increasingly smaller input String
. The base cases will remain unchanged, but the final branch of the when
block will differ:
1fun largestSeriesProductRecursive(number: String, n: Int, k: Int): Long {2 return when {3 n == 1 -> number.toLong()4 k == 1 -> number.maxOf { it.digitToInt() }.toLong()5 n == k -> stringProduct(number)6 else -> {7 maxOf(8 // first substring with k-adjacent digits9 stringProduct(number.take(k)),10 // provided string minus the first digit11 largestSeriesProductRecursive(number.drop(1), n - 1, k)12 )13 }14 }15}
Here are the average speeds for the two solutions over 100 iterations (using the original thousand-digit string), with the iterative solution outperforming the recursive one regardless of the size of :
k | Iterative (ns) | Recursive (ns) |
---|---|---|
4 | 2.5e+05 | 5.8e+05 |
13 | 2.3e+05 | 8.6e+05 |
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!🏝️🔎🎞️🐍