My wife, Dana, is the one working through Learning to Program and the
reason I
added the note to the quiz. Those watching the solutions will know that
she did
submit, but she also told me in a private message:
But I thought you should know that that problem was HARD!!!!!
I said in the quiz that you can do it with only the knowledge you’ve
gained in
the first eight chapters, and you can, but it does involve some tricky
steps
compared to the book problems. If you decide to build up the triangle
in memory
for example, you either need one long Array and some clever index work
to
determine where rows start and stop or you need to work with nested
Arrays.
Nested Arrays have a single example in chapter eight and the only the
briefest
of mentions, so that might be a bit of a leap.
However, if you hound your pet geek, he’ll give you enough hints to get
through
it. Here’s the proof, Dana Gray’s solution:
rows = ARGV[0].to_i
triangle = []
if rows >= 1
triangle.push([1])
end
if rows >= 2
triangle.push([1, 1])
end
last_row = [1, 1]
count = 3
while count <= rows
next_row = [1]
index = 0
while index < last_row.length - 1
next_row.push last_row[index]+last_row[index + 1]
index = index + 1
end
next_row.push(1)
triangle.push(next_row)
last_row = next_row
count = count + 1
end
number_length = last_row[last_row.length / 2].to_s.length
triangle_length = last_row.length * number_length + last_row.length - 1
triangle.each do |row|
final_row = []
row.each do | number|
final_row.push(number.to_s.center(number_length))
end
puts final_row.join(' ').center(triangle_length)
end
The first line is the one I gave to handle the command-line argument.
I’m not
sure if Learn to Program ever covers those, but it hasn’t by chapter
eight.
It’s not too tricky though; ARGV is just an Array holding the arguments
passed
to your program.
In the next couple of lines, Dana creates an Array to hold the triangle,
and
pushes the first two rows (as long as we are going that far). She then
declares
the last_row shortcut, to save typing triangle[triangle.length - 1] all
over the
place. That’s the only way Learn to Program has taught to fetch the
last
element of an Array, by chapter eight.
In the middle section of code, Dana builds up Pascal’s Triangle row by
row. The
code works by creating a next_row with the leading 1 already in it,
walking the
last_row to add up numbers for next_row, and slotting the final 1.
Then,
last_row is replaced with next_row, so the next run of the loop will
have the
new data to work with.
The final chunk of the code finds the length of the largest number in
the
triangle and the length of the longest row in the triangle and then uses
those
to print results. Note that an extra Array is used here to copy the
numbers
into while converting them to Strings and sizing them correctly, before
the
result is join()ed and sent to the screen.
Dana’s output doesn’t exactly match the quiz examples, because only one
space is
used to separate numbers where the quiz uses as many spaces as there are
digits
in the largest number. If we want to do that, we need to change the
line length
calculation to account for the bigger spaces:
triangle_length = last_row.length * number_length +
(last_row.length - 1) * number_length
and add the extra spaces to the final output:
puts final_row.join(' ' * number_length).center(triangle_length)
Now, I have great news for all you Learn to Program students. The more
you
learn, the easier this problem gets. You will be surprised how the code
just
melts away. To show what I mean, here’s a solution from Matthew M.
using a
very similar process but with a couple of iterators not yet covered in
the book:
def pascal n
rows = []
# generate data
(0...n).each do |i|
rows << if i.zero?
[1]
else
rows[i-1].inject([0]) do |m, o|
m[0...-1] << (m[-1] + o) << o
end
end
end
# calc field width
width = rows[-1].max.to_s.length
# space out each row
rows.collect! do |row|
row.collect { |x| x.to_s.center(2 * width) }.join
end
# display triangle
rows.each { |row| puts row.center(rows[-1].length) }
end
pascal (ARGV[0] || 10).to_i
This is the same process we examined before, with less variable
maintenance
work. Here the triangle is built with some clever use of inject() that
uses the
last slot of the row being built as a kind of short term memory that
gets shaved
back off as it is used each time (note the … in the 0 to -1 Range).
Converting the rows is also much easier here, thanks to the collect()
iterator.
Now, my good friend Greg Brown tells me real programmers use formulas to
build
the triangle. I’m just an imaginary programmer sadly, but Matthew M.
must be
real. Here’s another Matthew solution, this one filled with
formula-slinging
code:
class Integer
def fact
zero? ? 1 : (1..self).inject { |m, o| m * o }
end
def binom(k)
self.fact / (k.fact * (self - k).fact)
end
end
def pascal n
# calc field width
width = (n - 1).binom(n / 2).to_s.length
# keep only one row in memory
row = [1]
1.upto(n) do |i|
# print row
space = ' ' * width * (n-i)
puts space + row.collect { |x| x.to_s.center(2*width) }.join
# generate next row
row = row.inject([0]) { |m, o| m[0...-1] << (m[-1] + o) << o }
end
end
pascal (ARGV[0] || 10).to_i
What is of interest here are the fact() and binom() methods. This is
the
binomial coefficient formula that can be used to derive any number in
the
triangle.
More interesting to me though, is that Matthew only uses this to fetch a
single
number. You can see that the first action of the pascal() method uses
the
formula to conjure the middle element of the last row, for field width
purposes.
Once you have that, you can build the triangle normally and print rows
as you
go, as Matthew does here, because you have all the information you need
to
format them correctly.
Now folks, those are only three solutions of over 50! You should
definitely
poke around in the others. Make sure you catch the clever fixes people
used to
clean up triangle output, like left justifying the left side while right
justifying the right side. Don’t miss the example that makes use of
multi-methods and certainly don’t skip the first ever de-optimized Ruby
Quiz
solution. Fun tricks are hiding in so many of the solutions, you must
give them
a glance.
My thanks to Pascal’s fans who appear to be legion in numbers.
Tomorrow we will try something a little different. Instead of
celebrating
Ruby’s niceties over other common languages, we will see if we can’t
dumb it
down a bit and put some of the limits back…