Problem 93 — Arithmetic Expressions

solved Sat, 19 Jul 2025, 01:37 · level 4 · difficulty 13% projecteuler.net github

By using each of the digits from the set, $\{1, 2, 3, 4\}$, exactly once, and making use of the four arithmetic operations ($+, -, \times, /$) and brackets/parentheses, it is possible to form different positive integer targets.

For example,

$$\begin{align} 8 &= (4 \times (1 + 3)) / 2\\ 14 &= 4 \times (3 + 1 / 2)\\ 19 &= 4 \times (2 + 3) - 1\\ 36 &= 3 \times 4 \times (2 + 1) \end{align}$$

Note that concatenations of the digits, like $12 + 34$, are not allowed.

Using the set, $\{1, 2, 3, 4\}$, it is possible to obtain thirty-one different target numbers of which $36$ is the maximum, and each of the numbers $1$ to $28$ can be obtained before encountering the first non-expressible number.

Find the set of four distinct digits, $a \lt b \lt c \lt d$, for which the longest set of consecutive positive integers, $1$ to $n$, can be obtained, giving your answer as a string: abcd.


Problem Analysis

This problem has two layers. The outer layer is a small combinatorial search: I must consider every set of four distinct digits chosen from 1 to 9, of which there are C(9, 4) = 126. For each set I determine which positive integers are expressible, then measure the length of the unbroken run 1, 2, 3, ..., n. The set that maximises that run wins, and the answer is its digits written as a string.

The interesting layer is the inner enumeration. Given four numbers, I must explore every parenthesisation and every assignment of the four arithmetic operators to the gaps between them. The naive route is to enumerate all binary expression trees over four leaves and try each operator at each internal node - a fiddly counting exercise. The unlocking observation is that any arithmetic expression over a pool of k numbers can be built by repeatedly picking two numbers, combining them with one operation, and replacing both with the result. Each step shrinks the pool by one, so after k - 1 steps a single value remains. By iterating over every unordered pair and every applicable operation at each step, I visit every distinct expression tree exactly once - I never have to reason about tree shapes at all. Because subtraction and division are non-commutative, I consider both orderings: for a pair (a, b) I try the absolute difference |a - b| and both quotients a / b and b / a. The recursion is shallow (6 pairs, then 3, then 1), the input size is fixed at four, and the outer loop is just 126 iterations, so the whole program runs in effectively constant time. The only arithmetic subtlety is that division produces non-integers, so I carry all values as floating-point numbers and test for integrality only at the boundary.

Solution Approach

Recursive pool reduction (Python and C)

Both files implement the same algorithm. The core is a recursive routine that receives the current pool of values. If the pool holds a single element, that element is a reachable result and is returned. Otherwise the routine iterates over every unordered pair (i, j), removes those two elements to form a "rest" pool, and for each of the up to five operation results appends that result to the rest pool and recurses. The union of everything the recursive calls return is the full set of reachable values.

In Python (eval_all_operations) the pool is a tuple and the result is a set. The reduced pool is built with a comprehension that skips the two chosen indices, then I append each operation result and recurse. The guards if b > 0 and if a > 0 before the divisions avoid division by zero (a zero can arise from a subtraction) and also serve as light pruning. The caller solve loops over the 126 digit sets with four nested ranges enforcing a < b < c < d, filters the returned values for integers via x.is_integer(), then counts the consecutive run from 1 with a simple while length + 1 in results loop. I decorate the recursive function with functools.lru_cache: because the pool is an unordered multiset, the same intermediate state recurs across different call paths and different digit sets, and caching on the tuple of values lets me reuse that work.

The C version (eval_all) mirrors the algorithm without memoisation - the problem is small enough that plain recursion is fast. It uses a hand-rolled DoubleSet: a dynamically grown array of doubles with a linear-scan duplicate check before each insertion, plus an isfinite guard that discards any NaN or infinity. After collecting the reachable doubles for a digit set, the code marks each positive near-integer into a fixed boolean array found[2048] (rounding with floor(v + 0.5) and confirming with a 1e-9 tolerance), then scans forward from index 1 to measure the run. The outer loop structure matches the Python version exactly.

workbench

Test cases

categoryinputanswer
main 1258

Results

solutioncategoryanswerverdict avg seconds cavg seconds py
p0093_s0 main 1258 correct 0.022615 (63) 0.005026 (63)

Files

Tags & blogs