Hello Group,
This was quite an interesting quiz. I tried different solutions of varying complexity, but it turned out that the simplest approach worked best.
The problem is exponential in the number of source cards. It is solvable for six cards, but this algorithm is not a general solution for an arbitrary number of cards.
I recursively generate each possible term, and evaluate it. In my recursive generation process, each subterm is generated multiple times, so I used optional memoization to speed up the search. An exhaustive search takes around 300s without and 110s with memoization, but memoization uses a lot of ram.
If you only need one solution, it is usually found within 20s.
The full programm with commandline parsing and different implementations - Integer-Only vs. Float
- Brute Force vs. Memoization
can be found at:
http://ruby.brian-schroeder.de/quiz/
There is also a only version, that solves a random problem.
Following are the main routines of the program:
The Term structure is a direct implementation of the term-tree:
# Node of a Term Tree.
class Term
attr_reader :left, :right, :operation
def initialize(left, right, operation)
@left = left
@right = right
@operation = operation
end
def to_s
"(#{@left} #{operation} #{@right})"
end
def value
@value || (@value = (@left.value).send(@operation, (@right.value)))
end
def ==(o)
return false unless o.is_a?Term
to_s == o.to_s
end
end
The methods I use to create all the terms are:
Brute Force:
def Integral.each_term_over(source)
if source.length == 1
yield source[0]
else
source.each_partition do | p1, p2 |
each_term_over(p1) do | op1 |
yield op1
each_term_over(p2) do | op2 |
yield op2
if op2.value != 0
yield Term.new(op1, op2, :+)
yield Term.new(op1, op2, :-)
yield Term.new(op1, op2, :'/') if op2.value != 1 and op1.value % op2.value == 0
end
if op1.value != 0
yield Term.new(op2, op1, :-)
if op1.value != 1
yield Term.new(op2, op1, :'/') if op2.value % op1.value == 0
yield Term.new(op1, op2, :*) if op2.value != 0 and op2.value != 1
end
end
end
end
end
end
end
With Memoization:
def Integral.each_term_over(source, memo = {}, &block)
return memo[source] if memo[source]
result = []
if source.length == 1
result << source[0]
else
source.each_partition do | p1, p2 |
each_term_over(p1, memo, &block).each do | op1 |
each_term_over(p2, memo, &block).each do | op2 |
if op2.value != 0
result << Term.new(op1, op2, :+)
result << Term.new(op1, op2, :-)
result << Term.new(op1, op2, :'/') if op2.value != 1 and op1.value % op2.value == 0
end
if op1.value != 0
result << Term.new(op2, op1, :-)
if op1.value != 1
result << Term.new(op2, op1, :'/') if op2.value % op1.value == 0
result << Term.new(op1, op2, :*) if op2.value != 0 and op2.value != 1
end
end
end
end
end
end
result.each do | term | block.call(term) end
memo[source] = result
end
# Extending Array with simple set functions
class Array
# Returns each true partition (containing no empty set) exactly once.
def each_partition
return nil if empty?
head, *tail = *self
tail._each_partition do | subset, rest |
yield [subset.unshift(head), rest] unless rest.empty?
end
end
protected
# Returns each partition twice, as (s1, s2) and (s2, s1)
def _each_partition
if empty?
yield([], [])
return nil
end
head, *tail = *self
tail._each_partition do | s1, s2 |
yield([head] + s1, s2)
yield(s1, [head] + s2)
end
end
end
--
Brian Schröäer
http://www.brian-schroeder.de/