Here's the general programming solution to these kinds of problems. It also prints out for every result, how many solutions there are, which is useful in coming up with these kinds of problem:
from enum import IntEnum
from fractions import Fraction
from memoized import memoized
import math
NUMBERS = [6, 5, 4, 3]
RESULT = Fraction(1)
class Levels(IntEnum):
ADDITION = 0
MULTIPLICATION = 1
NEGATION = 2
POWER = 3
FACTORIAL = 4
BARE = 5
class Result:
def __init__(self, value, way, level):
"""
* value is the value achieved.
* way is string representation of an expression.
* level is the outermost operator applied to the expression, which
determines how this expression should be braketed when it's part of a
larger expression.
"""
assert isinstance(value, Fraction)
assert isinstance(level, Levels)
self.value = value
self.way = way
self.level = level
def bracketed(self, for_level):
if for_level > self.level:
return f"({self.way})"
return self.way
def combine(left_rs, right_rs):
for l in left_rs:
for r in right_rs:
if r.level not in (Levels.NEGATION, Levels.ADDITION):
# We block a - (-b), and a + (-b) since these can be written as
# a + b, and a - b. We also block a + (b + c), and a - (b + c)
# since these can be written as a + b + c, and a - b - c.
yield Result(l.value + r.value,
f"{l.bracketed(Levels.ADDITION)} + {r.bracketed(Levels.ADDITION + 1)}",
Levels.ADDITION)
yield Result(l.value - r.value,
f"{l.bracketed(Levels.ADDITION)} - {r.bracketed(Levels.ADDITION + 1)}",
Levels.ADDITION)
if r.level != Levels.MULTIPLICATION:
# We block a * (b * c) since this can be written as a * b * c.
# We block a * (b / c) since this can be written as a * b / c.
yield Result(l.value * r.value,
f"{l.bracketed(Levels.MULTIPLICATION)} * {r.bracketed(Levels.MULTIPLICATION + 1)}",
Levels.MULTIPLICATION)
if r.value != 0 and (r.level != Levels.MULTIPLICATION):
# We block a / (b / c) since this can be written as a / b * c.
# We block a / (b * c) since this can be written as a / b / c.
yield Result(l.value / r.value,
f"{l.bracketed(Levels.MULTIPLICATION)} / {r.bracketed(Levels.MULTIPLICATION + 1)}",
Levels.MULTIPLICATION)
new_value = None
power_okay = l.value >= 0 or r.value.denominator % 2 == 1
if l.value == 1:
new_value = Fraction(1)
elif l.value == -1:
if power_okay:
new_value = Fraction(-1
if r.value.numerator % 2 == 1
else 1)
elif l.value == 0:
if r.value > 0:
new_value = Fraction(0)
else:
# Only whole number powers are allowed for numbers other than
# -1, 0, 1.
if (power_okay
and r.value.denominator == 1
and -8 < r.value.numerator < 8):
new_value = l.value ** r.value
if new_value is not None:
# The left side is bracketed when it's a power because power is
# right associative.
yield Result(new_value,
f"{l.bracketed(Levels.POWER + 1)} ^ {r.bracketed(Levels.POWER)}",
Levels.POWER)
if (l.level == Levels.BARE
and r.level == Levels.BARE and len(r.way) == 1):
# We concatenate 123 as 1(23), but not as (12)3.
yield Result(l.value * 10 + r.value,
f"{l.way}{r.way}",
Levels.BARE)
def negate(rs):
for r in rs:
yield r
if r.level > Levels.MULTIPLICATION:
# We block -(a + b) since this can be written -a - b.
# We block -(a - b) since this can be written -a + b.
# We block -(a * b) since this can be written -a * b.
# We block -(a / b) since this can be written -a / b.
yield Result(-r.value,
f"-{r.bracketed(Levels.NEGATION)}",
Levels.NEGATION)
def factorialize(rs):
for r in rs:
yield r
x = 0
while (r.value.denominator == 1
and (r.value.numerator == 0 or (3 <= r.value.numerator <= 20))):
# We block fractional and negative factorials.
# We block 1 and 2 factorial.
# We block gigantic factorials.
r = Result(Fraction(math.factorial(r.value.numerator)),
f"{r.bracketed(Levels.FACTORIAL + 1)}!",
Levels.FACTORIAL)
yield r
# memoization makes the recursive solution into a dynamic programming solution.
@memoized(hashable=False)
def do(digits):
"""
Given a list of digits, produce a list of Result objects.
"""
l = len(digits)
if l == 1:
# Return a Result object for a single digit.
new_rs = [Result(Fraction(digits[0]), str(digits[0]), Levels.BARE)]
else:
# The list of results for a list of digits is the total results for every contiguous partition.
new_rs = []
for i in range(1, l):
new_rs.extend(list(combine(do(digits[:i]),
do(digits[i:]))))
return list(factorialize(negate(new_rs)))
rs = do(NUMBERS)
vs = {}
for r in rs:
vs.setdefault(r.value, []).append(r)
print(f"Solutions to {NUMBERS} = {RESULT}")
for r in sorted(vs[RESULT], key=lambda r: len(r.way)):
print(r.way)
print()
print(f" value solutions")
for v, l in sorted(vs.items(), key=lambda vl: (len(vl[1]), len(str(vl[0])))):
print(f"{str(v):>30} {len(l):<12}")
prints
Solutions to [6, 5, 4, 3] = 1
65 - 4 ^ 3
(6 - 5) ^ 43
65 + (-4) ^ 3
-(-6 + 5) ^ 43
-6 - 5 + 4 * 3
6 + 5 - 4 - 3!
6 - 5 * (4 - 3)
6 - 5 / (4 - 3)
6 - 5 ^ (4 - 3)
6 / (5 + 4 - 3)
6 - 5! / 4 / 3!
(6 - 5) ^ 4 ^ 3
(6 - 5) ^ (-43)
...
(((-6 + 5 + 4)!)! - (3!)!)!
((6! / (5! / 4!)!)! - (3!)!)!
((-(-6) ^ (5 - 4))! - (3!)!)!