Today I found a very interesting
set of 'simple' problems. It is a list of Berkeley University's "hardcore tech-interview style riddles and mathematical puzzles". I was specially interested in the Computer Science section (as you might have guessed) and, in particular, one problem caught my attention (probably because of the large matrix).
The problemThe problem reads as follows:
Write a program that will display a "spiral" of NxN numbers, using constant space (no arrays allowed). For example, here's what the spiral looks like for N=10:
99 98 97 96 95 94 93 92 91 90
64 63 62 61 60 59 58 57 56 89
65 36 35 34 33 32 31 30 55 88
66 37 16 15 14 13 12 29 54 87
67 38 17 4 3 2 11 28 53 86
68 39 18 5 0 1 10 27 52 85
69 40 19 6 7 8 9 26 51 84
70 41 20 21 22 23 24 25 50 83
71 42 43 44 45 46 47 48 49 82
72 73 74 75 76 77 78 79 80 81
Sounds simple enough.
The solutionAs I've been reading a lot of "Real World Haskell" lately (and considering the language is great for the task at hand) I decided to code this program in Haskell. If you don't already know Haskell I recommend you to go and learn it on your free time. It is an excellent language with a very unorthodox view of programming (not only functional, but pure!) that will open your mind to a very new and interesting programming style. A good resource for learning Haskell (besides the book, which I highly recommend and will do so with further details in a future post about books) is the classical
Gentle introduction to Haskell. But if you prefer a nice colourful website with cool drawings and good info to match the cool design (I know I do!) you can check
Learn you a Haskell for great good.
Enough with the language. Into the problem.
To analyse the solution I enumerated some of the matrices, from which matrices of size 4 and 5 were the most useful
15, 14, 13, 12,
4, 3, 2, 11,
5, 0, 1, 10,
6, 7, 8, 9
24, 23, 22, 21, 20,
9, 8, 7, 6, 19,
10, 1, 0, 5, 18,
11, 2, 3, 4, 17,
12, 13, 14, 15, 16
A matrix of size n contains the numbers from n*n-1 to 0. So the next matrix will contain 2n-1 new elements and the rest will be the same elements in a different order. This gives us the idea that the algorithm can be solved recursively.
If we look with more detail into the two matrices outlined above, we can notice that the top row and rightmost column contains all the new elements and that, due to the spiral ordering described in the problem, they will always be located in these positions (there are 2n-1 slots there).
What's left is to figure out how the elements in the previous matrix map to those in the next one and we'll have ourselves a recursive algorithm. We can notice that the first element in the matrix of size 4 is the last element in the sub-matrix formed by removing the new elements from the matrix of size 5. Because of the spiral order the next element to the right in the old matrix becomes the next to the left in the new one and the element that was beneath a number is transposed above it.
We can see it as a 90 degree rotation on the old matrix.
The codeKnowing this all that's left is to write the code.
We will need a function that given the position in the n-sized matrix will return the element
that belongs to that position.
spiral :: Int -> Int -> Int -> Int
spiral 0 j n = n*n - 1 - j
spiral i j n
| j == (n-1) = n*n - n - i
|otherwise = spiral (n-1-i) (n-j-2) (n-1)
The first line of this function (after the signature) is one of our base cases: The top row of the matrix is formed by the numbers from (n*n)-1 to n*(n-1). The second definition our second base case: the rightmost column. In this case each element descends from the element in the top right corner (i.e.: n*(n-1) ) one number by row. The rest of the function (the otherwise clause) is our recursive case. On the matrix of size n-1, we are interested in the element that occupies the row n2-i where n2 is the size of the smaller matrix (i.e.: n-1) and the column n2-j-1. Note that the difference between the row and the column is that in the column case we need to substract an extra 1 which refers to the extra columns that appears in the larger matrix. If we don't do this, the transformation would be transposed to the right.
Extra
This code is enough to solve the problem. But to better calculate the results (without having to calculate each index at a time) I provided some extra functions.
-- claculates an index based on the previous matrix
spiral :: Int -> Int -> Int -> Int
spiral 0 j n = n*n - 1 - j
spiral i j n
| j == (n-1) = n*n - n - i
|otherwise = spiral (n-1-i) (n-j-2) (n-1)
-- returns the numbers from 0 to n-1
range :: Int -> [Int]
range n = take n (iterate (1+) 0)
-- returns every possible index of an NxN matrix
matrix :: Int -> [(Int,Int)]
matrix n = [(i,j) | i<-range n, j<-range n] -- a helper function for the map in printSpiral revSp :: Int -> (Int, Int) -> Int
revSp n (i,j) = spiral i j n
-- returns the result for each index
printSpiral :: Int -> [Int]
printSpiral n = map (revSp n) (matrix n)
-- calculates all spiral matrices from 0 to n
through :: Int -> [[Int]]
through n = map printSpiral (range (n+1))
Does anyone have a better (or different) solution?