So you have a NxN grid and you want to extract all horizontal, vertical and diagonal lines of length M, then to find maximum product. Let's illustrate some Haskell techniques on example 4x4 grid, with line length being 2:
[[ 1, 2, 3, 4],
[ 5, 6, 7, 8],
[ 9,10,11,12],
[13,14,15,16]]
Horizontal and vertical is easy, all you need is a function that extract chunks of length M from a list:
chunks 2 [1,2,3,4] == [[1,2],[2,3],[3,4]]
The type of such function is [a] -> [[a]]
. This is a list-related function, so before reinventing the wheel, let's see if there's something similar in Data.List. Aha, tails
is similar, it returns lists with more and more elements from the beginning of the list removed:
tails [1,2,3,4] == [[1,2,3,4],[2,3,4],[3,4],[4],[]]
If only we could shorten the sublists to make them of length 2. But we can, by using map
function, which applies a function to every element of the list and returns a new list:
map (take n) (tails xs) -- [[1,2],[2,3],[3,4],[4],[]]
I wouldn't worry about smaller lines, as the original task is to find the biggest product, and product of [15, N]
≥ product of [15]
, N ≥ 1. But if you want to get rid of them, it seems that a list of length N contains N-M+1 chunks of length M, so you could apply take (4-2+1)
to the resulting list. Alternatively you could simply filter the list:
chunks n xs = filter ((==n) . length) $ map (take n) (tails xs)
-- [[1,2],[2,3],[3,4]]
Ok, we can extract a list of chunks from a list, but we have a 2D-grid, not a flat list! map
again rescues us:
map (chunks 2) grid -- [[[1,2],[2,3],[3,4]],[[5,6],[6,7],[7,8]],...]
But here's the thing, the resulting code puts chunks in separate lists, and it complicates things, as we don't actually care, from which line does the chunk originate. So we would want to flatten one level the resulting list by concat . map
or equivalent concatMap
:
concatMap (chunks 2) grid -- [[1,2],[2,3],[3,4],[5,6],[6,7],[7,8],...]
Now, how do I get vertical chunks from a grid? Sounds scary at first, until you realize that you can transpose the whole grid, i.e. turn rows into columns and columns into rows, and then apply the same code:
concatMap (chunks 2) (transpose grid) -- [[1,5],[5,9],[9,13],[2,6],[6,10],...]
Now the hard part: the diagonal lines. Norman Ramsey gives an idea: what if you could drop 0 elements from line 0, 1 elements from line 1, etc? The diagonal line would become a vertical line, which is easy to extract. You remember that to apply a function to every element of a list you use map
, but here you need to apply different functions to a each element, namely drop 0
, drop 1
, drop 2
, etc. map
wouldn't suit. But look, the first argument to drop
forms a pattern of successive numbers, which may be represented as an infinite list [0..]
. Now what if we could take one element from [0..]
What we need is a function that takes a number from an infinite list [0..]
and a row from the grid, and applies drop
with this number to the row. zipWith
is what you need:
zipWith drop [0..] grid -- [[1,2,3,4],[6,7,8],[11,12],[16]]
map head $ zipWith drop [0..] grid -- [1,6,11,16]
But I want all diagonals of length 2, not just the biggest diagonal. So look at the grid and think, what diagonal lines you see with elements on row 0? [1,6],[2,7],[3,8]
. So it's clear that you need to take only first 2 rows and transpose elements:
transpose $ zipWith drop [0,1] grid -- [[1,6],[2,7],[3,8],[4]]
Now how do I get diagonals starting from other rows as well? Remember our tails
trick? We can get all diagonals by providing our new function to a concatMap
and applying it to tails grid
:
concatMap (transpose . zipWith drop [0,1]) (tails g)
-- [[1,6],[2,7],[3,8],[5,10],[6,11],...]
But these are only diagonals that go from top-left to bottom-right. What about those that go from top-right to bottom-left? It's easiest just to reverse the rows of the grid:
concatMap (transpose . zipWith drop [0,1]) (tails $ reverse g)
-- [[13,10],[14,11],[15,12],[9,6],[10,7],...]
Finally, you need to find products of all lines and choose the biggest. The final code looks like:
grid = [[1..4],[5..8],[9..12],[13..16]]
chunks n xs = map (take n) (tails xs)
horizontal = concatMap (chunks 2) grid
vertical = concatMap (chunks 2) (transpose grid)
grave = concatMap (transpose . zipWith drop [0,1]) (tails grid)
acute = concatMap (transpose . zipWith drop [0,1]) (tails $ reverse grid)
maxProduct = maximum $ map product $ horizontal ++ vertical ++ grave ++ acute
-- answer: 240
Is this code maximally elegant and efficient? Hell no, but it works and illustrates certain patterns of thinking of functional programming. At first you need to write code that just works, then iteratively refactor it, until you arrive at a solution that is both easy to read and general.