24 Game is a classic backtracking puzzle. It teaches the core backtracking move: when you have many choices, try one, recurse, and undo it if it fails. The state shrinks each step, so the recursion always bottoms out.
Problem. You are given four cards, each with a number. Using +, −, ×, ÷ and parentheses,
can you make the four numbers evaluate to 24? Each number is used exactly once. Return True or False.
Example: cards = [1, 5, 5, 5] → True, because 5 × (5 − 1 / 5) = 24. We model the same search as
repeatedly picking two numbers and combining them.
The slow way first
There is no clever formula here — you genuinely have to try combinations. The naive instinct is to write out every fully-parenthesized arithmetic expression over the four numbers, which is fiddly and easy to get wrong with operator precedence.
The cleaner framing: instead of building one big expression, repeatedly collapse two numbers into one. Pick any two numbers, apply an operator, and you now have a smaller list. Keep going until one number is left. If that number is 24, you found an expression.
The idea: pick two, recurse, backtrack
At each step, try every ordered pair of numbers and every operator (order matters for − and ÷). Replace the pair with the result and recurse on the shorter list. If the recursion succeeds, propagate True up. If it fails, undo that choice and try the next operator or pair. When the list is down to one number, check if it is ≈ 24.
Why floating point and a tolerance? Division produces fractions (like 5 − 1/5), so we compare with abs(x − 24) < 1e-6 rather than == 24.
Walk through it
Step through the animation. We start with [1, 5, 5, 5]. We pick a pair, the cells collapse to a shorter list, and the op label shows the operator chosen. One branch dead-ends from [4, 25], so we backtrack and try a different operator on the 5s. Following the winning branch, the numbers reduce all the way down to a single 24.
Pseudocode
solve(cards):
if only one number left:
return it is approximately 24
for each ordered pair (a, b) in cards:
rest = cards with a and b removed
for each result of combining a and b (+, -, *, /, both orders):
if solve(rest + [result]):
return True # this choice worked
return False # every choice failed -> backtrackThe Python solution
def judge_point_24(cards):
if len(cards) == 1:
return abs(cards[0] - 24) < 1e-6
for i in range(len(cards)):
for j in range(len(cards)):
if i == j:
continue
rest = [cards[k] for k in range(len(cards)) if k != i and k != j]
for val in results(cards[i], cards[j]):
if judge_point_24(rest + [val]):
return True
return False- The base case is
len(cards) == 1: one number left, so check if it is within1e-6of 24. - The two loops over
iandjpick an ordered pair of positions — order matters becausea − b ≠ b − a. restis the list with both chosen numbers removed.results(a, b)yields every value of combining them:a+b,a−b,a×b, anda/b(skipping division by zero). Because the pair is ordered, both subtraction and division orders get covered.- We recurse on
rest + [val]. If any branch returnsTrue, we return immediately. If none do, we fall through toreturn False, which backtracks to the caller to try its next option.
Complexity
| Case | Time | Notes |
|---|---|---|
| Pairs per level | O(n²) (slow) | ordered pairs of numbers |
| Operators per pair | O(1) (fast) | at most 6 distinct results |
| Whole search (n = 4) | O(1) (fast) | bounded constant work |
O(n) (moderate)For the fixed four-card game the search tree is small and bounded, so the whole thing is effectively constant time. The space is the recursion depth, which is at most the number of cards.
When this pattern shows up
Whenever a problem says "try all ways to combine / split / arrange" and each choice shrinks the problem, reach for backtracking: loop over choices, recurse on the reduced state, and undo on failure. Letter combinations, permutations, and expression-building all share this skeleton.
Two easy bugs: forgetting the ordered pair (so you miss b − a and b / a), and comparing with
exact equality. Division makes fractions, so always compare against 24 with a small tolerance, not ==.
Practice
From the list [4, 25], can any single operator reach 24? List the four results.
1. What is the base case of the recursion?
2. Why do we loop over ordered pairs (both i, j and j, i)?
3. What does the algorithm do when a branch fails to reach 24?
4. Why compare with abs(x - 24) < 1e-6 instead of x == 24?