On The Universe of Discourse Mark Dominus discusses the classic 24 Puzzle:
You are given a sequence of four digits, say 1, 2, 3, 4, and your job is to combine them with ordinary arithmetic operations (+, -, ×, and ÷) in any order to make a target number, typically 24. For example, with 1, 2, 3, 4, you can go with
((1+2)+3)×4=24
or with4×((2×3)×1)=24
.
Here’s a solver for such puzzles. It uses itertools
to generate possible expressions, fractions
to get the arithmetic right, and eval
to evaluate expressions. It’s limited to expressions formed from 4 numbers which means I don’t have to programmatically calculate different ways of parenthesising: there are only 5.
# http://blog.plover.com/math/24-puzzle.html import re import math # Use fractions for exact calculations from fractions import Fraction # Solve for 4 numbers only! N = 4 # So these are the only expression templates # where X is a number and @ is an operation templates = '''\ ((X @ X) @ X) @ X (X @ (X @ X)) @ X X @ ((X @ X) @ X) X @ (X @ (X @ X)) (X @ X) @ (X @ X)'''.splitlines() import itertools as its def defrac(s): return re.compile(r'Fraction\((\d+)\)').sub(r'\1', s) def evaluate(nums, ops, template): fracs = ('Fraction(%s)' % n for n in nums) ops = iter(ops) expr = ''.join(next(fracs) if c == 'X' else next(ops) if c == '@' else c for c in template) try: return expr, eval(expr) except ZeroDivisionError: return expr, None def solve(spec, ops): numbers = re.compile(r'\d+').findall(spec) assert len(numbers) == N + 1 result = Fraction(numbers.pop()) seqs = its.product(its.permutations(numbers), its.product(ops, repeat=N-1), templates) print(defrac(next((e for e, v in its.starmap(evaluate, seqs) if v == result), 'Impossible'))) def main(): solve('2,5,6,6 => 17', '+-/*') solve('3,3,8,8 => 24', '+-/*') main()