views:

184

answers:

4

I'm currently working on a project Euler problem (www.projecteuler.net) for fun but have hit a stumbling block. One of the problem provides a 20x20 grid of numbers and asks for the greatest product of 4 numbers on a straight line. This line can be either horizontal, vertical, or diagonal.

Using a procedural language I'd have no problem solving this, but part of my motivation for doing these problems in the first place is to gain more experience and learn more Haskell.
As of right now I'm reading in the grid and converting it to a list of list of ints, eg -- [[Int]]. This makes the horizontal multiplication trivial, and by transposing this grid the vertical also becomes trivial.

The diagonal is what is giving me trouble. I've thought of a few ways where I could use explicit array slicing or indexing, to get a solution, but it seems overly complicated and hackey. I believe there is probably an elegant, functional solution here, and I'd love to hear what others can come up with.

+1  A: 

You can use the !! function to retrieve elements in a list by index. That with a fixed step either incrementing or decrementing the index gets you a diagonal.

MSN
Random access on lists is O(n) though. Of course that's probably not a problem for a mere 20x20 grid.
sepp2k
This was what I was referring to when I mentioned being able to solve it using indexing and slicing. Maybe this is the best solution, I just had an intuition that there was something more elegant I could try.
untwisted
+2  A: 

Lists are the wrong data structure for this problem, as they don't provide random indexing in constant time -- they bias towards linear traversals. So your diagonals will always be more annoying/slower with lists.

How about using arrays? E.g. parallel vectors or regular vectors.

Don Stewart
I haven't really looked much into alternative data structures yet, though would these provide anything for me in terms of ease of implementation or just efficiency and speed?By the way, thanks for the great book, it is a large part of the reason I'm as far along in my Haskell learning as I am :)
untwisted
@untwisted: it should be easier to implement your solution using an array type, because you can index your array as a tuple (x,y) coordinate, so if you are using haskell's vanilla `Data.Array` library, you would have a type of `Array (Int,Int) Int`. This will also be much faster unless you have a really clever algorithm using [[Int]].
jberryman
Thanks for the clarification, that would make it much easier than the [[Int]] to work with.
untwisted
+5  A: 
Norman Ramsey
I was thinking of solving this over the weekend and your example shows (validates?) the approach I was planning to take :)
Tim Perry
" the last thing you want is to futz around with array indexing" -- the array libraries like vector are based on combinators, so its no worse than the list API in ease of use. But I take your point on 20x20.
Don Stewart
@Don: Oh, nice. I followed your vector link and was overwhelmed by the number of yummy vector functions available.
Norman Ramsey
I like the basic idea here, but when I try to develop it to get all diagonals (both types), I get a rather hairy code ball, whose efficiency is awful. The big problem is that the request length is less than the grid size. Hence, you need a separate "triangularization" for each row.
MtnViewMark
+2  A: 

Well, for this particular problem, a single linear list or array is actually the easiest structure! The key is to think about these runs as skipping through the list with a given stride. If the grid is w × h in size, then

  • a horizontal run has a stride of 1
  • a vertical run has a stride of w
  • one diagonal run has a stride of w-1
  • one diagonal run has a stride of w+1

Now, for each of the four kinds of runs, you just need to compute the possible starting points. Something like this:

allRuns :: Int -> Int -> Int -> [a] -> [[a]]
allRuns n w h es = horiz ++ vert ++ acute ++ grave
    where horiz = runs [0..w-n]   [0..h-1] 1
          vert  = runs [0..w-1]   [0..h-n] w
          acute = runs [n-1..w-1] [0..h-n] (w-1)
          grave = runs [0..w-n]   [0..h-n] (w+1)

          runs xs ys s = [run (x+y*w) s | x <- xs, y <- ys]
          run i s = map (es!!) [i,i+s..i+(n-1)*s]

Of course, in an efficient implementation, you'd replace the [a] with something like Data.Array Int a and es!! with es!

MtnViewMark
Index arithmetic is FORTRAN, not Haskell.
Norman Ramsey
Indeed it is, but in this case, the problem *is* essentially one of indexing. I've yet to see a solution that generates all runs that is this short and clear. I think, in the end, this expresses what the problem is after rather directly. And I think *that* is the essence of Haskell!
MtnViewMark