Largest Product in a Grid
Project Euler presents the following Problem #11:
In the 20×20 grid below, four numbers along a diagonal line have been marked in red.
The product of these numbers is 26 x 63 x 78 x 14 = 1788696.
What is the greatest product of four adjacent numbers in the same direction (up, down, left, right, or diagonally) in the 20×20 grid?
Helper Functions
For the solution to this problem, we can use a nested List<List<Int>> to store the provided numbers since there is no requirement to mutate the contents of the collection. But how can we build this representation of a grid?
Assuming that the grid is being read as input from a .txt file (with space-separated values), we can use the following function to process each line:
1import java.io.File23/**4 * Retrieves content of a test resource file, with each line transformed into a nested list.5 *6 * @param [lineTrim] characters to remove from the left & right of each file line.7 * @param [lineSplit] characters to use as the delimiter when splitting a line.8 * @param [transformation] function that takes either an entire line as an9 * argument or, if split, individual elements in a line.10 */11fun <R> getTestResource(12 filePath: String,13 lineTrim: CharArray = charArrayOf(' ', '\n'),14 lineSplit: String = " ",15 transformation: (String) -> R16): List<List<R>> {17 return File(filePath).useLines { lines ->18 lines.map { line ->19 line.trim(*lineTrim).split(lineSplit).map(transformation)20 }.toList()21 }22}2324fun main() {25 val grid = getTestResource("path/to/file.txt", transformation = String::toInt)26}
Line 11: This helper is set up as a generic function to ensure reusability across all Project Euler test files.
Line 17: useLines() returns the final value of the block callback and closes the reader once processing is complete.
Line 19: Here's a reminder of how Kotlin's spread operator works when a vararg-function, such as trim(), is called.
N.B. getTestResource() throws a FileNotFoundException if the pathname provided does not match an existing resource, so consider handling this if appropriate.
Now that we have a grid, let's also implement a small extension function to calculate the product of numbers in a List:
1fun List<Int>.product(): Int = reduce { acc, n -> acc * n }
N.B. This extension function will throw an UnsupportedOperationException if the collection is empty, which will not happen in our case. There is also no possibility of integer overflow given the problem's small numbers. Consider also handling these edge cases if choosing to use this function elsewhere.
Solution
The problem suggests multiple directions to search for the potential largest product: "up, down, left, right, or diagonally". Each direction can, of course, only be checked if the position of the current element allows for the expected amount of adjacent numbers. However, if we move methodically through each element in the grid, it can be observed that choosing to search all proposed directions leads to duplicate searches:

Instead, the directions can be reduced by the associative property of multiplication to: down, right, leading diagonal, and counter diagonal:

Note that the terms found by searching "down" are essentially the equivalent of searching right from the same position using a transposed grid.
Once the directions to search have been established, it's just a matter of getting the product of all applicable terms and storing the maximum value across each iteration:
1fun largestProductInGrid(grid: List<List<Int>>, terms: Int = 4): Int {2 var largest = 03 for (row in grid.indices) {4 for (col in 0..(grid[0].size - terms)) {5 val right = grid[row].slice(col until (col + terms)).product()6 val down = List(terms) { grid[col+it][row] }.product()7 var leadingDiag = 08 var counterDiag = 09 if (row <= grid.size - terms) {10 leadingDiag = List(terms) { grid[row+it][col+it] }.product()11 counterDiag = List(terms) { grid[row+it][col+terms-1-it] }.product()12 }13 largest = maxOf(largest, right, down, leadingDiag, counterDiag)14 }15 }16 return largest17}
Slice vs Sublist
slice() and
subList() are interchangeable on line 5 of largestProductInGrid() because the grid is read-only; however, these functions are by no means the same.
slice() always returns an immutable List, whereas subList() returns a view of the original collection that will reflect any non-structural changes to either the original or the view. If the elements of our grid were mutable, we would need to consider the potential side effects of our choice.
Using slice() would result in no reflection when the original grid changes:
1fun main() {2 val grid = List(4) { MutableList(4) { 0 } }3 val portion: List<Int> = grid[0].slice(1..3)4 grid.forEach { println(it) }5 println("Slice -> $portion")6 println("--------")7 repeat(3) { grid[0][it + 1] = 1 }8 grid.forEach { println(it) }9 println("Slice -> $portion")10}
[0, 0, 0, 0][0, 0, 0, 0][0, 0, 0, 0][0, 0, 0, 0]Slice -> [0, 0, 0]--------[0, 1, 1, 1][0, 0, 0, 0][0, 0, 0, 0][0, 0, 0, 0]Slice -> [0, 0, 0]
The same behavior would not be achieved using subList():
1fun main() {2 val grid = List(4) { MutableList(4) { 0 } }3 val portion: MutableList<Int> = grid[0].subList(1, 4)4 grid.forEach { println(it) }5 println("Sublist -> $portion")6 println("--------")7 repeat(3) { grid[0][it + 1] = 1 }8 grid.forEach { println(it) }9 println("Sublist -> $portion")10 println("--------")11 portion[0] = 212 grid.forEach { println(it) }13 println("Sublist -> $portion")14}
[0, 0, 0, 0][0, 0, 0, 0][0, 0, 0, 0][0, 0, 0, 0]Sublist -> [0, 0, 0]--------[0, 1, 1, 1][0, 0, 0, 0][0, 0, 0, 0][0, 0, 0, 0]Sublist -> [1, 1, 1]--------[0, 2, 1, 1][0, 0, 0, 0][0, 0, 0, 0][0, 0, 0, 0]Sublist -> [2, 1, 1]
📚 Grids are a fairly common feature in Project Euler problems and, from here onwards, will require more than brute force iteration to solve in a reasonable time. Instead, algorithms, like breadth-first search and dynamic programming, will be heavily relied on and we'll even get a chance to be clever with combinatorics from as early as Problem #15.
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!🏝️🔎🧭