D
Dennis Ranke
Hi, here is my second solution for this very interesting quiz.
My first solution took an estimated one hour in the worst case (when no
exact solution exists), this one is A LOT faster. The actual time
depends a bit on the input values, but I have never seen it take more
than 8 seconds.
This solution works by combining two sub terms at a time into new
composite terms until either a term with the target value is found or
all combinations have been generated.
The most important optimization was to ignore a new term if another term
of the same value using the same input numbers has already been found.
To further improve the speed I also decided not to allow terms giving
fractions, zero or negative values.
So here it is:
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
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'
@term_hashes.each_with_index do |hash, index|
next if term.mask & index != 0
# iterate through the hashes and build composite terms using
# the four basic operators
hash.each_value do |other|
[:+, :-, :*, :/].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
mask = term.mask | other.mask
next if @term_hashes[mask].has_key?(value) ||
new_hashes[mask].has_key?(value)
new_term = Term.new(value, 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_hashes[mask][value] = new_term
end
end
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
My first solution took an estimated one hour in the worst case (when no
exact solution exists), this one is A LOT faster. The actual time
depends a bit on the input values, but I have never seen it take more
than 8 seconds.
This solution works by combining two sub terms at a time into new
composite terms until either a term with the target value is found or
all combinations have been generated.
The most important optimization was to ignore a new term if another term
of the same value using the same input numbers has already been found.
To further improve the speed I also decided not to allow terms giving
fractions, zero or negative values.
So here it is:
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
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'
@term_hashes.each_with_index do |hash, index|
next if term.mask & index != 0
# iterate through the hashes and build composite terms using
# the four basic operators
hash.each_value do |other|
[:+, :-, :*, :/].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
mask = term.mask | other.mask
next if @term_hashes[mask].has_key?(value) ||
new_hashes[mask].has_key?(value)
new_term = Term.new(value, 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_hashes[mask][value] = new_term
end
end
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