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