My solution to the quiz below belongs to this same family of
solutions. I, like Daniel, actually solved the set of equations via
matrix transformations, although my code is self-contained and quick
'n dirty, not nicely refactored like his.
When multiple families of word equation solutions exist, my program
chooses one of them randomly to display.
Eric
----------------
Are you interested in on-site Ruby training that's been highly
reviewed by former students? http://LearnRuby.com
================
require 'mathn'
CompactOutput = false
# calculate the least common multiple of one or more numbers
def lcm(first, *rest)
rest.inject(first) { |l, n| l.lcm(n) }
end
# Returns nil if there is no solution or an array containing two
# elements, one for the left side of the equation and one for the
# right side. Each of those elements is itself an array containing
# pairs, where each pair is an array in which the first element is the
# number of times that word appears and the second element is the
# word.
def solve_to_array(words)
# clean up word list by eliminating non-letters, converting to lower
# case, and removing duplicate words
words.map! { |word| word.downcase.gsub(/[^a-z]/, '') }.uniq!
# calculate the letters used in the set of words
letters = Hash.new
words.each do |word|
word.split('').each { |letter| letters[letter] = true }
end
# create a matrix to represent a set of linear equations.
column_count = words.size
row_count = letters.size
equations = []
letters.keys.each do |letter|
letter_counts = []
words.each { |word| letter_counts << word.count(letter) }
equations << letter_counts
end
# transform matrix into row echelon form
equations.size.times do |row|
# re-order the rows, so the row with a value in then next column
# to process is above those that contain zeroes
equations.sort! do |row1, row2|
column = 0
column += 1 until column == column_count ||
row2[column].abs != row1[column].abs
if column == column_count : 0
else row2[column].abs <=> row1[column].abs
end
end
# figure out which column to work on
column = (0...column_count).detect { |i| equations[row][i] != 0 }
break unless column
# transform rows below the current row so that there is a zero in
# the column being worked on
((row + 1)...equations.size).each do |row2|
factor = -equations[row2][column] / equations[row][column]
(column...column_count).each do |c|
equations[row2][c] += factor * equations[row][c]
end
end
end
# only one of the free variables chosen randomly will get a 1, the
# rest 0
rank = equations.select { |row| row.any? { |v| v != 0 }}.size
free = equations[0].size - rank
free_values = Array.new(free, 0)
free_values[rand(free)] = 2 * rand(2) - 1
values = Array.new(equations[0].size) # holds the word_counts
# use backward elimination to find values for the variables; process
# each row in reverse order
equations.reverse_each do |row|
# determine number of free variables for the given row
free_variables = (0...column_count).inject(0) do |sum, index|
row[index] != 0 && values[index].nil? ? sum + 1 : sum
end
# on this row, 1 free variable will be calculated, the others will
# get the predetermined free values; the one being calculated is
# marked with nil
free_values.insert(rand(free_variables), nil) if free_variables >
0
# assign values to the variables
sum = 0
calc_index = nil
row.each_index do |index|
if row[index] != 0
if values[index].nil?
values[index] = free_values.shift
# determine if this is a calculated or given free value
if values[index] : sum += values[index] * row[index]
else calc_index = index
end
else
sum += values[index] * row[index]
end
end
end
# calculate the remaining value on the row
values[calc_index] = -sum / row[calc_index] if calc_index
end
if values.all? { |v| v } && values.any? { |v| v != 0 }
# in case we ended up with any non-integer values, multiply all
# values by their collective least common multiple of the
# denominators
multiplier =
lcm(*values.map { |v| v.kind_of?(Rational) ? v.denominator :
1 })
values.map! { |v| v * multiplier }
# deivide the terms into each side of the equation depending on
# whether the value is positive or negative
left, right = [], []
values.each_index do |i|
if values[i] > 0 : left << [values[i], words[i]]
elsif values[i] < 0 : right << [-values[i], words[i]]
end
end
[left, right] # return found equation
else
nil # return no found equation
end
end
# Returns a string containing a solution if one exists; otherwise
# returns nil. The returned string can be in either compact or
# non-compact form depending on the CompactOutput boolean constant.
def solve_to_string(words)
result = solve_to_array(words)
if result
if CompactOutput
result.map do |side|
side.map { |term| "#{term[0]}*\"#{term[1]}\"" }.join(' + ')
end.join(" == ")
else
result.map do |side|
side.map { |term| (["\"#{term[1]}\""] * term[0]).join(' +
') }.
join(' + ')
end.join(" == ")
end
else
nil
end
end
if __FILE__ == $0 # if run from the command line...
# collect words from STDIN
words = []
while line = gets
words << line.chomp
end
result = solve_to_string(words)
if result : puts result
else exit 1
end
end