#!/usr/bin/env python
# coding: utf-8
# Peter Norvig
5 January 2016
revised 18 May 2018
# # Four 4s, Five 5s, and Countdown to 2016
#
# On January 1, 2016 Alex Bellos [posed](http://www.theguardian.com/science/2016/jan/04/can-you-solve-it-complete-the-equation-10-9-8-7-6-5-4-3-2-1-2016) this New Year's puzzle:
#
#
# > Fill in the blanks so that this equation makes arithmetical sense:
# >
# > `10 ␣ 9 ␣ 8 ␣ 7 ␣ 6 ␣ 5 ␣ 4 ␣ 3 ␣ 2 ␣ 1 = 2016`
# >
# > You are allowed to use *only* the four basic arithmetical operations: +, -, ×, ÷. But brackets (parentheses) can be used wherever needed. So, for example, the solution could begin
# >
# > `(10 + 9) * (8` ... or `10 + (9 * 8)` ...
#
# Let's see if we can solve this puzzle, and some of the related ones from Alex's [first](http://www.theguardian.com/science/2016/jan/04/can-you-solve-it-complete-the-equation-10-9-8-7-6-5-4-3-2-1-2016) and [second](http://www.theguardian.com/science/2016/jan/04/did-you-solve-it-complete-the-equation-10-9-8-7-6-5-4-3-2-1-2016) post.
# # Four Operations, No Brackets
#
# We'll start with a simpler version of the puzzle: a countdown with no brackets.
#
# There are nine blanks, each of which can be filled by one of four operators, so there are 94 = 262,144 possibilities, few enough that we can enumerate them all, using `itertools.product` to get sequences of operators, and then `str.format` to plug them into blanks, and then `eval` to evaluate the string:
# In[1]:
from itertools import product
from functools import lru_cache # Used later
from collections import Counter, defaultdict # Used later
# In[2]:
ops = next(product(('+', '-', '*', '/'), repeat=9))
ops
# In[3]:
'10{}9{}8{}7{}6{}5{}4{}3{}2{}1'.format(*ops)
# In[4]:
eval(_)
# We need to catch errors such as dividing by zero, so I'll define a wrapper function, `evaluate`, to do that, and I'll define `simple_countdown` to put the pieces together:
# In[5]:
def evaluate(exp):
"eval(exp), or None if there is an arithmetic error."
try:
return eval(exp)
except ArithmeticError:
return None
def simple_countdown(target, operators=('+', '-', '*', '/')):
"All solutions to the countdown puzzle (with no brackets)."
exps = ('10{}9{}8{}7{}6{}5{}4{}3{}2{}1'.format(*ops)
for ops in product(operators, repeat=9))
return [exp for exp in exps if evaluate(exp) == target]
# In[6]:
simple_countdown(2016)
# Too bad; we did all that work and didn't find a solution. What years *can* we find solutions for? I'll modify `simple_countdown` to take a collection of target years rather than a single one, and return a dict of the form `{year: 'expression'}` for each expression that evaluates to one of the target years. I'll also generalize it to allow any format string, not just the countdown string.
# In[7]:
def simple_countdown(targets, operators=('+', '-', '*', '/'),
fmt='10{}9{}8{}7{}6{}5{}4{}3{}2{}1'):
"All solutions to the countdown puzzle (with no brackets)."
exps = (fmt.format(*ops)
for ops in product(operators, repeat=fmt.count('{}')))
return {int(evaluate(exp)): exp for exp in exps if evaluate(exp) in targets}
simple_countdown(range(1900, 2100))
# Interesting: in the 20th and 21st centuries, there are only two "golden eras" where the bracketless countdown equation works: the three year period centered on 1980, and the seven year period that is centered on 2016, but omits 2016.
# # Four Operations, With Brackets
#
# Now we return to the original problem. Can I make use of what I have so far? Well, my second version of `simple_countdown` accepts different format strings, so I could give it all possible bracketed format strings and let it fill in all possible operator combinations. How long would that take? I happen to remember that the number of bracketings of *n* operands is the nth [Catalan number](https://oeis.org/A000108), which I looked up and found to be 4862. A single call to `simple_countdown` took about 2 seconds, so we could do 4862 calls in about 160 minutes. That's not terrible, but (a) it would still take work to generate all the bracketings, and (b) I think I can find another way that is much faster.
#
# I'll restate the problem as: given the sequence of numbers (10, 9, ... 1), create an expression whose value is 2016.
# But to get there I'll solve a more general problem: given any sequence of numbers, like `(10, 9, 8)`, what `{value: expression}` pairs can I make with them?
# I'll define `expressions(numbers)` to return a dict of `{value: expression}`
# for all expressions (strings) whose numeric value is `value`, and can be made from `numbers`, for example:
#
# expressions((10,)) ⇒ {10: '10'}
# expressions((9, 8)) ⇒ {1: '(9-8)', 1.125: '(9/8)', 17: '(9+8)', 72: '(9*8)'}
#
# I'll use the idea of [dynamic programming](https://en.wikipedia.org/wiki/Dynamic_programming): break the problem down into simpler subparts, compute an answer for each subpart, and remember intermediate results so we don't need to re-compute them later. How do we break the problem into parts? `expressions((10, 9, 8))` should consist of all the ways of splitting `(10, 9, 8)` into two parts, finding all the expressions that can be made with each part, and combining pairs of expressions with any of the four operators:
#
# expressions((10, 9, 8)) ⇒ {11: '(10+(9-8))', 27: '(10+(9+8))', 720: '(10*(9*8))', ...}
#
# First the function `splits`:
# In[8]:
c10 = (10, 9, 8, 7, 6, 5, 4, 3, 2, 1)
def splits(sequence):
"Split sequence into two non-empty parts, in all ways."
return [(sequence[:i], sequence[i:])
for i in range(1, len(sequence))]
# In[9]:
splits(c10)
# Now the function `expressions`:
# In[10]:
@lru_cache(None)
def expressions(numbers: tuple) -> dict:
"Return {value: expr} for all expressions that can be made from numbers."
if len(numbers) == 1:
return {numbers[0]: str(numbers[0])}
else:
result = {}
for (Lnums, Rnums) in splits(numbers):
for (L, R) in product(expressions(Lnums), expressions(Rnums)):
Lexp = '(' + expressions(Lnums)[L]
Rexp = expressions(Rnums)[R] + ')'
result[L * R] = Lexp + '*' + Rexp
result[L - R] = Lexp + '-' + Rexp
result[L + R] = Lexp + '+' + Rexp
if R != 0:
result[L / R] = Lexp + '/' + Rexp
return result
# For example, at one point in a call to `expressions((10, 9, 8))` we would have:
#
# Lnums, Rnums = (10), (9, 8)
# L, R = 10, 1
# Lexp, Rexp = '(10', '(9-8))'
#
# This would lead to us adding the following four entries to `result`:
#
# result[10] = '(10*(9-8))'
# result[9] = '(10-(9-8))'
# result[11] = '(10+(9-8))'
# result[10] = '(10/(9-8))'
#
# The decorator `@lru_cache` takes care of storing the intermediate results. Rather than catching errors, we just avoid division by 0.
#
# Let's give it a try:
# In[11]:
expressions((10,))
# In[12]:
expressions((9, 8))
# In[13]:
expressions((10, 9, 8))
# That looks reasonable. Let's solve the whole puzzle.
#
# # Countdown to 2016: A Solution
# In[14]:
get_ipython().run_line_magic('time', 'expressions(c10)[2016]')
# We have an answer! And in seconds, not hours, thanks to dynamic programming! Here are solutions for nearby years:
# In[15]:
{y: expressions(c10)[y] for y in range(2010, 2026)}
# # Counting Solutions
#
# Alex Bellos had another challenge:
#
# > I was half hoping a computer scientist would let me know exactly how many solutions [to the Countdown problem] there are with only the four basic operations. Maybe someone will.
#
# As it stands, my program can't answer that question, because I only keep one expression for each value.
#
# Also, I'm not sure what it means to be a distinct solution. For example, are `((10+9)+8)` and `(10+(9+8))` different, or are they same, because they both are equivalent to `(10+9+8)`? Similarly, are `((3-2)-1)` and `(3-(2+1)` different, or the same because they both are equivalent to `(3 + -2 + -1)`? I think the notion of "distinct solution" is just inherently ambiguous. My choice is to count each of these as distinct: every expression has exactly ten numbers, nine operators, and nine pairs of brackets, and if an expression differs in any character, it is different. But I won't argue with anyone who prefers a different definition of "distinct solution."
#
# So how can I count expressions? One approach would be to go back to enumerating every equation (all 4862 × 49 = 1.2 bilion of them) and checking which ones equal 2016. That would take about 40 hours with my Python program. A better approach is to mimic `expressions`, but to make a table of counts, rather than expression strings. I want:
#
# counts[(10, 9, 8)][27] == 2
#
# because there are 2 ways to make 27 with the numbers `(10, 9, 8)`, namely, `((10+9)+8)` and `(10+(9+8))`. And in general, if there are `Lcount` ways to make `L` using `Lnums` and `Rcount` ways to make `R` using `Rnums` then there are `Lcount * Rcount` ways of making `L * R` using `Lnums` followed by `Rnums`. So we have:
#
# In[16]:
@lru_cache(None)
def counts(numbers: tuple) -> Counter:
"Return a Counter of {value: count} for every value that can be made from numbers."
if len(numbers) == 1: # Only one way to make an expression out of a single number
return Counter(numbers)
else:
result = Counter()
for (Lnums, Rnums) in splits(numbers):
for L in counts(Lnums):
for R in counts(Rnums):
count = counts(Lnums)[L] * counts(Rnums)[R]
result[L + R] += count
result[L - R] += count
result[L * R] += count
if R != 0:
result[L / R] += count
return result
# In[17]:
counts((2, 2)) # corresponds to {0: '2-2', 1.0: '2/2', 4: '2+2' or '2*2'}
# In[18]:
counts((10, 9, 8))[27] # (10+9)+8 or 10+(9+8)
# Looks good to me. Now let's see if we can answer the question.
# In[19]:
counts(c10)[2016]
# This says there are 30,066 distinct expressions for 2016.
#
# **But we're forgetting about round-off error.**
#
# Let's find all the values that are very near to `2016`:
# In[20]:
{y: counts(c10)[y]
for y in counts(c10)
if abs(y - 2016) < 1e-10}
# I suspect that all of these actually should be exactly 2016.
# To be absolutely sure, I could re-do the calculations using exact rational arithmetic, as provided by the `fractions.Fraction` data type. From experience I know that would be an order of magnitude slower, so instead I'll just add up the counts:
# In[21]:
sum(_.values())
# I have more confidence in this answer, 44,499, than in 30,066, but I wouldn't accept it as definitive until it was independently verified and passed an extensive test suite. And of course, if you have a different definition of "distinct solution," you will get a different answer.
# # Four 4s
#
# Alex Bellos continues with a related puzzle:
#
# > The most famous “fill in the gaps in the equation” puzzle is known as the [four fours](https://en.wikipedia.org/wiki/Four_fours), because every equation is of the form
# >
# > `4 ␣ 4 ␣ 4 ␣ 4 = X.`
# >
# >In the classic form of the puzzle you must find a solution for X = 0 to 9 using just addition, subtraction, multiplication and division.
#
# This puzzle goes back to a [1914 publication](https://archive.org/details/mathematicalrecr00ball) by the "most famous" mathematician/magician W. W. Rouse Ball. The solution is easy:
# In[22]:
{i: expressions((4, 4, 4, 4))[i] for i in range(10)}
# Note that I didn't do anything special to take advantage of the fact that in `(4, 4, 4, 4)` the digits are all the same. Happily, `lru_cache` does that for me automatically! If I split that into `(4, 4)` and `(4, 4)`, when it comes time to do the second half of the split, the result will already be in the cache, and so won't be recomputed.
# # New Mathematical Operations
#
# Bellos then writes:
#
# > If you want to show off, you can introduce **new mathematical operations** such as powers, square roots, concatenation and decimals, ... or use the factorial symbol, `!`.
#
# Bellos is suggesting the following operations:
#
# - **Powers**: `(4 ^ 4)` is 4 to the fourth power, or 256.
# - **Square roots:** `√4` is the square root of 4, or 2.
# - **Concatenation:** `44` is forty-four.
# - **Decimals:** `4.4` is four and four tenths.
# - **Factorials** `4!` is 4 × 3 × 2 × 1, or 24.
#
# There are some complications to deal with:
#
# - **Irrationals**: `√2` is an irrational number; so we can't do exact rational arithmetic.
# - **Imaginaries**: `√-1` is an imaginary number; do we want to deal with that?
# - **Overflow**: `(10. ^ (9. ^ 8.))`, as a `float`, gives an `OverflowError`.
# - **Out of memory**: [`(10 ^ (9 ^ (8 * 7)))`](http://www.wolframalpha.com/input/?i=10+%5E+9+%5E+56), as an `int`, gives an `OutOfMemoryError` (even if your memory uses every atom on Earth).
# - **Infinite expressions**: We could add an infinite number of square root and/or factorial signs to any expression: `√√√√√√(4!!!!!!!!)`...
# - **Round-off error**: `(49*(1/49))` evaluates to `0.9999999999999999`, not `1`.
#
# We could try to manage this with *symbolic algebra*, perhaps using [SymPy](http://www.sympy.org/en/index.html), but that seems complicated, so instead I will make some arbitrary decisions:
#
# - Use floating point approximations for everything, rational or irrational.
# - Disallow irrationals.
# - Disallow overflow errors (a type of arithmetic error).
# - Put limits on what is allow for exponentiation.
# - Allow only two nested unary operations to avoid infinite expressions.
#
# I'll define the function `do` to do an arithmetic computation, catch any errors, and try to correct round-off errors. The idea is that since my expressions start with integers, any result that is very close to an integer is probably actually that integer. So I'll correct `(49*(1/49))` to be `1.0`. Of course, an expression like `(1+(10^-99))` is also very close to `1.0`, but it should not be rounded off. Instead, I'll try to avoid such expressions by silently dropping any value that is outside the range of 10-10 to 1010 (except of course I will accept an exact 0).
# In[23]:
from operator import add, sub, mul, truediv
from math import sqrt, factorial
def do(op, *args):
"Return op(*args), trying to correct for roundoff error, or `None` on Error or too big/small."
try:
val = op(*args)
return (val if val == 0 else
None if isinstance(val, complex) else
None if not (1e-10 < abs(val) < 1e10) else
round(val) if abs(val - round(val)) < 1e-12 else
val)
except (ArithmeticError, ValueError):
return None
# In[24]:
assert do(truediv, 12, 4) == 3 and do(truediv, 12, 0) == None
assert do(mul, 49, do(truediv, 1, 49)) == 1
assert do(pow, 10, -99) == None
# I'll take this opportunity to refactor `expressions` to have these subfunctions:
# - `digit_expressions(result, numbers)`: returns a dict like {0.44: '.44', 4.4: '4.4', 44.0: '44'}
# - `add_unary_expressions(result)`: add expressions like `√44` and `4!` to `result` and return `result`.
# - `add_binary_expressions(result, numbers)`: add expressions like `4+√44` and `4^4` to `result` and return `result`.
# In[25]:
@lru_cache(None)
def expressions(numbers: tuple) -> dict:
"Return {value: expr} for all expressions that can be made from numbers."
return add_unary_expressions(add_binary_expressions(digit_expressions(numbers), numbers))
def digit_expressions(digits: tuple) -> dict:
"All ways of making numbers from these digits (in order), and a decimal point."
D = ''.join(map(str, digits))
exps = [(D[:i] + '.' + D[i:]).rstrip('.')
for i in range(len(D) + 1)
if not (D.startswith('0') and i > 1)]
return {float(exp): exp for exp in exps}
def add_binary_expressions(result: dict, numbers: tuple) -> dict:
"Add binary expressions by splitting numbers and combining with an op."
for (Lnums, Rnums) in splits(numbers):
for (L, R) in product(expressions(Lnums), expressions(Rnums)):
Lexp = '(' + expressions(Lnums)[L]
Rexp = expressions(Rnums)[R] + ')'
assign(result, do(truediv, L, R), Lexp + '/' + Rexp)
assign(result, do(mul, L, R), Lexp + '*' + Rexp)
assign(result, do(add, L, R), Lexp + '+' + Rexp)
assign(result, do(sub, L, R), Lexp + '-' + Rexp)
if -10 <= R <= 10 and (L > 0 or int(R) == R):
assign(result, do(pow, L, R), Lexp + '^' + Rexp)
return result
def add_unary_expressions(result: dict, nesting_level=2) -> dict:
"Add unary expressions: -v, √v and v! to result"
for _ in range(nesting_level):
for v in tuple(result):
exp = result[v]
if -v not in result:
assign(result, -v, '-' + exp)
if 0 < v <= 100 and 120 * v == round(120 * v):
assign(result, sqrt(v), '√' + exp)
if 3 <= v <= 6 and v == int(v):
assign(result, factorial(v), exp + '!')
return result
# The function `assign` will silently drop expressions whose value is `None`, and if two expressions have the same value, `assign` keeps the one with the lower "weight"—a measure of the characters in the expression. This shows simpler expressions in favor of complex ones: for four 4s, the entry for `0` will be `44-44`, not something like `-√((-√4*--4)*(-√4--√4))`.
# In[26]:
def assign(result: dict, value: float, exp: str):
"Assign result[value] = exp, unless we already have a lighter exp or value is None."
if (value is not None
and (value not in result or weight(exp) < weight(result[value]))):
result[value] = exp
PENALTIES = defaultdict(int, {'√':7, '!':5, '.':1, '^':3, '/':1, '-':1})
def weight(exp: str) -> int: return len(exp) + sum(PENALTIES[c] for c in exp)
# I'll define a function to print a table of consecutive integers (starting at 0) that can be made by a sequence of numbers:
# In[27]:
def show(numbers: tuple, upto=10000, clear=True):
"""Print expressions for integers from 0 up to limit or the first unmakeable integer."""
if clear: expressions.cache_clear() # To free up memory
print('{:,} entries for {}\n'.format(len(expressions(numbers)), numbers))
for i in range(upto + 1):
if i not in expressions(numbers):
return i
print('{:4} = {}'.format(i, unbracket(expressions(numbers)[i])))
def unbracket(exp: str) -> str:
"Strip outer parens from exp, if they are there"
return (exp[1:-1] if exp.startswith('(') and exp.endswith(')') else exp)
# In[28]:
get_ipython().run_line_magic('time', 'show((4, 4, 4, 4))')
# We can also solve the "2016 with four fours" puzzle:
# In[29]:
expressions((4, 4, 4, 4))[2016]
# In a [separate video](https://www.youtube.com/embed/Noo4lN-vSvw), Alex Bellos shows how to form **every** integer from 0 to infinity using four 4s, if the square root and `log` functions are allowed. The solution comes from Paul Dirac (although [Dirac originally developed it](https://nebusresearch.wordpress.com/2014/04/18/how-dirac-made-every-number/) for the "four 2s" problem).
#
# Donald Knuth has [conjectured](https://www.tandfonline.com/doi/abs/10.1080/0025570X.1964.11975546) that with floor, square root and factorial, you can make any positive integer with just **one** 4.
#
# Below are some popular variants:
#
# # Four 2s
# In[30]:
show((2, 2, 2, 2))
# # Four 9s
# In[31]:
show((9, 9, 9, 9))
# # Four 5s
# In[32]:
show((5, 5, 5, 5))
# # Five 5s
# In[33]:
get_ipython().run_line_magic('time', 'show((5, 5, 5, 5, 5), False)')
#
# # Countdown to 2018
#
# One more thing: On January 1 2018, [Michael Littman](http://cs.brown.edu/~mlittman/) posted this:
#
# > 2+0+1×8, 2+0-1+8, (2+0-1)×8, |2-0-1-8|, -2-0+1×8, -(2+0+1-8), sqrt(|2+0-18|), 2+0+1^8, 20-18, 2^(0×18), 2×0×1×8... Happy New Year!
#
# Can we replicate that countdown? For 2018 and for following years?
# In[34]:
show((2,0,1,8), 10)
# In[35]:
show((2,0,1,9), 10)
# Now we run into a problem. For the year 2020, the two zeroes don't give us much to work with, and we can't make all the integers in the countdown. But I can turn a 0 into a 1 with `cos(0)`; maybe that will be enough? Let's try it. I'll modify `add_unary_expressions` to assign values for `sin` and `cos`:
# In[36]:
from math import sin, cos
def add_unary_expressions(result: dict, nesting_level=2) -> dict:
"Add unary expressions: -v, √v and v! to result dict."
for _ in range(nesting_level):
for v in tuple(result):
exp = result[v]
if -v not in result:
assign(result, -v, '-' + exp)
if 0 < v <= 100 and 120 * v == round(120 * v):
assign(result, sqrt(v), '√' + exp)
if 3 <= v <= 6 and v == int(v):
assign(result, factorial(v), exp + '!')
if abs(v) in (0, 45, 90, 180):
assign(result, sin(v), 'sin(' + exp + ')')
assign(result, cos(v), 'cos(' + exp + ')')
return result
PENALTIES.update(s=1, i=1, n=1, c=1, o=1)
# In[37]:
show((2, 0, 2, 0), 10, clear=True) # Clear the cache so we get new results for (2, 0) etc.
# In[38]:
show((2, 0, 2, 1), 10)
# # What's Next?
#
# One exercise would be adding even more operators, such as:
#
# - **Floor and Ceiling**: `⌊5.5⌋` = 5, `⌈5.5⌉` = 6
# - **Nth root**: `3√8` = 2
# - **Percent**: `5%` = 5/100
# - **Repeating decimal**: `.4...` = .44444444... = 4/9
# - **Double factorial**: `9!!` = 9 × 7 × 5 × 3 × 1 = 945
# - **Gamma function**: `Γ(n)` = (n − 1)!
# - **Prime counting function**: `π(n)` = number of primes ≲ n; `π(5)` = 3
# - **Transcendental functions**: besides `sin` and `cos`, there's `log`, `tan`, `arcsin`, ...
#
# In the [xkcd forum](http://forums.xkcd.com/viewtopic.php?f=14&t=116813&start=280) they got up to 298 for five fives, using the double factorial and π functions. What would you like to do?