"""
Python Countdown Numbers Game solver, Dan Goodman Jan 2008
Solves the Countdown Numbers game fairly quickly in a fairly neat way.
Usage:
------
In the following, target is an integer, sources is a list of integers, and
ops is a list of operations (functions taking two values), which if
unspecified is left as +,-,*,/. The aim is to find expressions constructed
from the integer sources and the operations which evaluate to the target.
-- findfirsttarget(target,sources(,ops))
Finds the first matching expression
-- targetexpressions(target,sources(,ops))
Generator, returns all the (string) expressions matching the target
-- expressions(sources(,ops))
Generator, returns all valid expressions (in list format), see its
docs for more info.
-- strexpression(e)
Converts a list format expression to a string expression
Examples:
---------
>>> findfirsttarget(234,[100,9,7,6,3,1])
'(100+(9-(1-(7*(3*6)))))'
>>> for x in targetexpressions(33,[100,3,1]):
print x
((100-1)/3)
Detailed notes:
---------------
Raw expressions generated by the expressions() generator are (recursive)
lists of 1 or 3 elements. A list with 1 element contains just an integer,
one of the sources. A list of three elements is of the form: [f,x,y] where
f is an operator and x and y are themselves raw expressions. So, for example
(x+y)*z would be represented as [mul,[add,x,y],z].
The standard operations are just +,-,*,/ but with some special features.
Add and multiply with raise a ValueError exception if their second
argument is bigger than their first argument. This stops the algorithm
from processing both a+b and b+a, which makes it run significantly faster.
The division operation raises the same exception if the result is a noninteger
or division by zero is attempted.
The algorithm is fairly simple minded, it just goes through every possible
expression recursively, evaluating as it goes. For each position in the
expression tree, it first tries each remaining source value, and then for
each operator f it tries f(x,y) for every possible pair of expressions
(x,y). See source code for the expressions() generator for a more detailed
explanation.
"""
__all__ = [ 'expressions', 'findfirsttarget', 'targetexpressions', 'strexpression' ]
sub = lambda x,y: x-y
def add(x,y):
if x<=y: return x+y
raise ValueError
def mul(x,y):
if x<=y or x==1 or y==1: return x*y
raise ValueError
def div(x,y):
if not y or x%y or y==1:
raise ValueError
return x/y
add.disp = '+'
mul.disp = '*'
sub.disp = '-'
div.disp = '/'
standard_ops = [ add, sub, mul, div ]
def strexpression(e):
"""Convert a raw expression to a string"""
if len(e)==3:
return '('+strexpression(e[1])+e[0].disp+strexpression(e[2])+')'
elif len(e)==1:
return str(e[0])
raise ValueError
# The algorithm for the expressions function works as follows:
# 1. Start with an empty expression tree with a single free slot
# 2. When recursively filling in a free slot, first try each
# of the remaining sources, then for each operation, build
# an extra branch using that operation and two free slots.
def expressions(sources,ops=standard_ops,minremsources=0):
"""Generates all expressions
Returns a tuple (e,s,v) where:
-- e, a raw expression
-- s, the remaining unused sources
-- v, the value of the expression
Example (from findfirsttarget):
for e,s,v in expressions(sources,ops):
if v==target:
return strexpression(e)
The minremsources argument to this generator is only used internally.
See source code comments for algorithmic notes.
"""
# First try each of the sources, when we try each one we have to remember
# to remove it from the list of remaining sources that we pass back
# and pass back the (trivial) evaluation of the subexpression, which is
# just the value of the source
for i in range(len(sources)):
# expression remaining sources value
yield ([sources[i]],sources[:i]+sources[i+1:],sources[i])
# Now we have the slightly more complicated recursive job of trying
# all possible subexpressions formed by an operation with two free
# slots. The first thing to check is that there are actually enough
# sources remaining. If you just check len(sources)>=2 you run into a
# problem. It seems like having 2 remaining sources is enough because
# you are adding two free slots which would have to be filled by
# sources. However, because the algorithm proceeds by first recursing
# on the left hand side, then the right hand side, you might actually
# need to leave some sources over for the calling function to use to
# fill in one of its remaining slots. If you don't check for this you
# get an infinite recursion. Let me illustrate, you have the following
# sources left [a,b,c] say. The algorithm first tries to generate:
# +(.,.) where . is a free slot. Now it tries +(+(.,.),.) and
# +(+(+(.,.),.),.) etc. because until those free slots have a source
# placed in them, no sources are used up. So, the solution is to
# keep track of the number of sources we will need to have left over
# after the operation is finished for all of the calling functions
# to fill their slots. We do this by incrementing minremsources, the
# minimum number of remaining sources required after this call yields
# an expression, for the expressions generated on the left hand side.
# We don't have to do this when generating expressions for the right
# hand side because when we get to doing this, the left hand side
# expressions will already have filled all their free slots. So, in
# conclusion, the following line checks that we have enough sources
# to fill the two free slots we'll create, and leave enough left over
# for the calling function to fill
if len(sources)>=2+minremsources:
# e1, rs1, v1 = LHS expression, remaining sources, value, we iterate
# over all expressions which leave us one extra remaining source to
# fill the right hand side of the expression we're generating
for e1, rs1, v1 in expressions(sources,ops,minremsources+1):
# similar to e1,rs1,v1
for e2, rs2, v2 in expressions(rs1,ops,minremsources):
# logically, this loop is outside the previous two, but
# for speed reasons it makes more sense to put it here.
# It tries out all the operations on its inputs. The
# clever thing to note is that if the operation
# o(v1,v2) raises a ValueError exception, we do not yield the
# expression (and thereby save on further processing).
for o in ops:
# [o,e1,e2] is the raw expression, rs2 is the sources
# remaining after the expressions e1 and e2 have been
# generated, and o(v1,v2) is the operation applied to
# the values of e1 and e2 (we compute as we go along
# to save time).
try: yield ([o,e1,e2],rs2,o(v1,v2))
except ValueError: pass
def findfirsttarget(target,sources,ops=standard_ops):
"""Find the first matching expression"""
for e,s,v in expressions(sources,ops):
if v==target:
return strexpression(e)
return []
def targetexpressions(target,sources,ops=standard_ops):
"""Generates all matching expressions"""
for e,s,v in expressions(sources,ops):
if v==target:
yield strexpression(e)
if __name__=='__main__':
import time
def demo(target,sources):
print "Looking for:",target
print "Given numbers:",sources
print "Operators:",
for o in standard_ops:
print o.disp,
print
print
start = time.time()
s = [e for e in targetexpressions(target,sources)]
s.sort(lambda x,y:len(x)-len(y))
print "Found",len(s),"solutions, minimal string length solution:"
print s[0]
print
print "Took", time.time()-start, "seconds."
print "A sample:"
print
demo(234,[100,9,7,6,3,1])
print
print "Now trying one of the most difficult possible problems:"
print
demo(923,[7,8,50,8,1,3])