Here is a small update to my solution. A simple optimization speeds up
the solver by a factor of nearly two. Now I'm down to 5 seconds for the
worst case on this machine :)
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
# 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(1 << @num_sources) { {} }
# 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 = (@target * 1000).abs
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(1 << @num_sources) { {} }
# 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 < 64
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]
[:+, :-, :*, :/].each do |op|
# don't allow fractions
# if you want to allow fractions remove this line and
# make sure that the source numbers are floats (or
# include rational.rb)
next if op == :/ && term.value % other.value != 0
# calculate value of composite term
value = term.value.send(op, other.value)
# don't allow negative values or zero
# (negative subterms are not necessairy as long as the
# target is positive)
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, term, other)
# 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