I have just found a bug in both versions of my fast solution. They are 
unable to find (for example) the solution 25-(2*3) = 19:

Dennis-Rankes-Computer:~/coding/ruby-quiz exo$ ruby 07-countdown2.rb 19 
25 2 3
[25, 2, 3] -> 19
(25 + 2) = 27 (error: 8)
(25 - 2) = 23 (error: 4)
(25 - 3) = 22 (error: 3)
((25 - 2) - 3) = 20 (error: 1)
0.016051 seconds

In addition, the optimized version had a bug that only let it cope with 
exactly 6 input numbers.

I have fixed both, which slows down the solution a slight bit (it takes 
about 7% longer to go through all combinations):

Dennis-Rankes-Computer:~/coding/ruby-quiz exo$ ruby 07-countdown4.rb 19 
25 2 3[25, 2, 3] -> 19
(25 + 2) = 27 (error: 8)
(25 - 2) = 23 (error: 4)
(25 - 3) = 22 (error: 3)
((25 - 2) - 3) = 20 (error: 1)
(25 - (3 * 2)) = 19 (error: 0)
0.016437 seconds

class Solver
   class Term
     attr_reader :value, :mask

     def initialize(value, mask, op = nil, left = nil, right = nil)
       @value = value
       @mask = mask
       @op = op
       @left = left
       @right = right
     end

     def to_s
       return @value.to_s unless @op
       "(#@left #@op #@right)"
     end
   end

   def initialize(sources, target)
     printf "%s -> %d\n", sources.inspect, target
     @target = target
     @new_terms = []
     @num_sources = sources.size
     @num_hashes = 1 << @num_sources

     # the hashes are used to check for duplicate terms
     # (terms that have the same value and use the same
     # source numbers)
     @term_hashes = Array.new(@num_hashes) { {} }

     # enter the source numbers as (simple) terms
     sources.each_with_index do |value, index|

       # each source number is represented by one bit in the bit mask
       mask = 1 << index
       term = Term.new(value, mask)
       @new_terms << term
       @term_hashes[mask][value] = term
     end
   end

   def run
     collision = 0
     best_difference = 1.0/0.0
     next_new_terms = [nil]
     until next_new_terms.empty?
       next_new_terms = []

       # temporary hashes for terms found in this iteration
       # (again to check for duplicates)
       new_hashes = Array.new(@num_hashes) { {} }

       # iterate through all the new terms (those that weren't yet used
       # to generate composite terms)
       @new_terms.each do |term|

         # iterate through the hashes and find those containing terms
         # that share no source numbers with 'term'
         index = 1
         term_mask = term.mask

         # skip over indeces that clash with term_mask
         index += collision - ((collision - 1) & index) while
                     (collision = term_mask & index) != 0
         while index < @num_hashes
           hash = @term_hashes[index]

           # iterate through the hashes and build composite terms using
           # the four basic operators
           hash.each_value do |other|
             new_mask = term_mask | other.mask
             hash = @term_hashes[new_mask]
             new_hash = new_hashes[new_mask]

             # sort the source terms so that the term with the larger
             # value is left
             # (we don't allow fractions and negative subterms are not
             # necessairy as long as the target is positive)
             if term.value > other.value
               left_term = term
               right_term = other
             else
               left_term = other
               right_term = term
             end
             [:+, :-, :*, :/].each do |op|

               # don't allow fractions
               next if op == :/ &&
                       left_term.value % right_term.value != 0

               # calculate value of composite term
               value = left_term.value.send(op, right_term.value)

               # don't allow zero
               next if value == 0

               # ignore this composite term if this value was already
               # found for a different term using the same source
               # numbers
               next if hash.has_key?(value) || new_hash.has_key?(value)

               new_term = Term.new(value, new_mask, op, left_term,
                                   right_term)

               # if the new term is closer to the target than the
               # best match so far print it out
               if (value - @target).abs < best_difference
                 best_difference = (value - @target).abs
                 printf "%s = %d (error: %d)\n", new_term, value,
                                     best_difference
                 return if best_difference == 0
               end

               # remember the new term for use in the next iteration
               next_new_terms << new_term
               new_hash[value] = new_term
             end
           end
           index += 1
           index += collision - ((collision - 1) & index) while
                       (collision = term_mask & index) != 0
         end
       end

       # merge the hashes with the new terms into the main hashes
       @term_hashes.each_with_index do |hash, index|
         hash.merge!(new_hashes[index])
       end

       # the newly found terms will be used in the next iteration
       @new_terms = next_new_terms
     end
   end
end

if ARGV[0] && ARGV[0].downcase == 'random'
   ARGV[0] = rand(900) + 100
   ARGV[1] = (rand(4) + 1) * 25
   5.times {|i| ARGV[i + 2] = rand(10) + 1}
end

if ARGV.size < 3
   puts "Usage: ruby countdown.rb <target> <source1> <source2> ..."
   puts "   or: ruby countdown.rb random"
   exit
end

start_time = Time.now
Solver.new(ARGV[1..-1].map {|v| v.to_i}, ARGV[0].to_i).run
printf "%f seconds\n", Time.now - start_time