I made a huge mistake this morning and summarized a quiz that Bob wanted to do himself. I'm so sorry about that.
Here's Bob's summary and I will get it on the site shortly...
James Edward Gray II
···
Begin forwarded message:
From: "Bob Showalter" <showaltb@gmail.com>
Date: January 18, 2007 8:53:36 AM CST
To: "James Edward Gray II" <james@grayproductions.net>
Subject: Ruby Quiz 109 SummaryJames,
Here is my summary for the Number Spiral quiz. Thanks, Bob
RubyQuiz 109
Number SpiralSummary
This was an interesting little puzzle, because it isn't immediately obvious
just how to write out a spiral line by line. The rule against building up the
data in an array was intended to make it bit more challenging, although I'm
not sure using an array simplifies things all that much.The solutions tended to fall into one of two groups, each taking a different
approach to the problem. The first group noticed that a spiral of any given
order contained within it all the spirals of lower orders. So, a 4x4 spiral
contains the 3x3, which in turn contains the 2x2, and so on. This leads to a
recursive algorithm.The other group derived a function that could generate the number to be
emitted for any "cell", given the order of the spiral and the cell's row and
column.Krishna Dole's compact solution is an example of the latter approach. Let's
analyze his algorithm:He defines a method called spiral, which takes a pair of coordinates that are
relative to the center of the spiral. At this point, I'm not sure what the
method does; we'll have to get to that later. I note that it doesn't take the
order of the spiral as a parameter, so we'll have to see how he's handling
that.The main program looks like this:
n = ARGV[0].to_i
for row in 0..(n - 1)
puts (0..(n - 1)).map{|col|
spiral(col - (n / 2), (n / 2) - row).to_s.rjust(4) }.join
endThe first line sets n to the "order" of the spiral as taken from the command
line argument. So n=5 would mean a 5x5 spiral.The for loop iterates over the rows of the spiral, starting at 0. So for our
5x5 spiral, we would have rows 0, 1, 2, 3, and 4.The next two lines pack a lot into a small space. If we ignore the call to
spiral for a moment, it looks like this:puts (0..(n - 1)).map{|col| ... }.join
The map operation iterates over the columns of the spiral (e.g. 0 through 4),
setting the variable col to the column number. The results of the block (i.e.
whatever the code represented by the ellipsis does) are concatenated by join
into a single string and then written out (followed by a newline) by puts.So we know that the code in the elipsis must give us a single "cell", and
these cells are strung together to make a row, which is output. Looking back
at the original code, we can see that the return from spiral is converted to a
string and right-justified in a 4-character field. So the return from spiral
is the number to be written in the current cell.OK, so lets look at how he calculates the number to go in a cell. Remember,
when spiral is called, row is the current row number (0=top) and col is the
current column number (0=left). The call to spiral looks like:spiral(col - (n / 2), (n /2) - row)
(n / 2) is a constant. It's half the size of the spiral. Note that
because n is an Integer, (n / 2) is also an Integer, so a value of 5
for n would yield 2 for (n / 2), and not 2.5.Remember the comment in the code above the definition of spiral? It told us
that the coordinates were relative to the center of the spiral. So this code
is adjusting the row and col values to be relative to the center. The top-left
corner of our 5x5 spiral would be row=0 and col=0. The values passed to spiral
would bex = col - (n / 2)
= 0 - (5 / 2)
= 0 - 2
= -2y = (n / 2) - row
= (5 / 2) - 0
= 2 - 0
= 2The subtraction is reversed for the y coordinate so that y values increase
moving "up" the screen even though row values increase moving "down".Why make the coordinates relative to the center of the spiral? Because for any
given (x, y) pair relative to the center of the spiral, the number to be
output at that position is the same, *regardless of the order of the spiral*.
So that is why we don't see the order referenced in the spiral method: it
isn't needed.OK, now it's time to figure out how the spiral method does its magic. Krishna
starts by computing two values max_xy, and offset:max_xy = [x,y].collect{|num| num.abs}.max
offset = (max_xy * 2 - 1)**2 - 1max_xy is obviously the larger of x or y, considered in absolute terms. If you
think of the spiral as a set of concentric "rings" surrounding the digit zero,
max_xy would tell us which "ring" we are on. This is part of the key to the
algorithm: he doesn't treat the spiral as a spiral at all, but as a set of
rings. For a spiral of odd order, the rings are completely filled in; for a
spiral of even order, the outermost ring is only partially filled.Each ring is a sequence of numbers. Here are the sequences for the first few
"rings":max_xy=0 0
max_xy=1 1..8
max_xy=2 9..24
max_xy=3 25..48offset is simply one less than the starting value of each ring. So for
max_xy=3, offset is 24.The remainder of the code is an "if" statement that evaluates to the number to
be output for the curent cell. It computes this by treating the "ring" as a
sequence of four "legs", and computing the value given the "leg" and position
within the leg. For the outermost ring of a 5x5 spiral, the "legs" look like
this:A B B B B
A . . . C
D D D D CThe different branches of the if statement handle each leg. Let's consider the
bottom-most "A" cell of this ring. It would have the coordinates (-2, -1). The
code for this leg is:if -(x) == max_xy and x != y
y + offset + max_xyThe test is true, because -(-2) == 2 and -2 != -1. So the value is (-1) + 24 +
2, or 25. The calculations for the remaining legs proceed in a similar manner.