The 24 Game is deceptively simple: given four numbers, combine them with +, -, ×, ÷ to make exactly 24. Sounds easy, right? Try solving 1, 5, 5, 5 — it stumps most people.
I built a complete solver for my math puzzle platform, and the algorithm turned out to be a great exercise in combinatorics, expression trees, and floating-point traps. Let me walk you through it.
The Problem Space
Four numbers, four operators, and parentheses. How many possibilities?
- Permutations of 4 numbers: up to 24 (4!)
- Operator combinations: 4³ = 64 (three slots, four choices each)
- Parenthesization patterns: 5 (the Catalan number C₃)
Total: 24 × 64 × 5 = 7,680 evaluations. Trivial for modern hardware.
The key insight is that we need to enumerate all five binary tree structures for combining four operands — these correspond to the five ways to fully parenthesize a sequence of four items.
The Five Expression Patterns
For numbers a, b, c, d and operators op1, op2, op3:
Pattern 1: ((a op1 b) op2 c) op3 d
Pattern 2: (a op1 (b op2 c)) op3 d
Pattern 3: (a op1 b) op2 (c op3 d)
Pattern 4: a op1 ((b op2 c) op3 d)
Pattern 5: a op1 (b op2 (c op3 d))
These five patterns cover every possible way to combine four numbers with binary operators. No expression is left unexplored.
The Algorithm in TypeScript
Step 1: Generate Permutations (with deduplication)
When input contains duplicates (like 5, 5, 5, 1), many permutations are identical. We use a Set to filter:
function permutations(arr: number[]): number[][] {
if (arr.length <= 1) return [arr];
const result: number[][] = [];
const seen = new Set<string>();
for (let i = 0; i < arr.length; i++) {
const rest = [...arr.slice(0, i), ...arr.slice(i + 1)];
for (const perm of permutations(rest)) {
const newPerm = [arr[i], ...perm];
const key = newPerm.join(',');
if (!seen.has(key)) {
seen.add(key);
result.push(newPerm);
}
}
}
return result;
}
For 5, 5, 5, 1, this reduces 24 permutations down to just 4.
Step 2: Evaluate Each Pattern
The core loop iterates over all permutations × operators × patterns:
const operators = ['+', '-', '*', '/'];
function operate(a: number, b: number, op: string): number {
switch (op) {
case '+': return a + b;
case '-': return a - b;
case '*': return a * b;
case '/': return b !== 0 ? a / b : Infinity;
default: return Infinity;
}
}
For each pattern, we evaluate step by step, short-circuiting on division by zero or invalid results:
// Pattern 3: (a op1 b) op2 (c op3 d)
try {
const r1 = operate(a, b, op1);
if (!isValidNum(r1)) throw new Error();
const r2 = operate(c, d, op3);
if (!isValidNum(r2)) throw new Error();
const r3 = operate(r1, r2, op2);
if (!isValidNum(r3)) throw new Error();
if (Math.abs(r3 - 24) < 0.0001) {
// Found a solution!
}
} catch {}
Step 3: Handle Floating-Point Precision
This is the tricky part. When 8 / (3 - 8/3) = 24, the intermediate results involve fractions. Direct equality check (r3 === 24) will fail due to floating-point errors.
Solution: use an epsilon comparison:
if (Math.abs(r3 - 24) < 0.0001) {
// This is a valid solution
}
This tiny tolerance (0.0001) handles all the edge cases while avoiding false positives.
Putting It All Together
export function solve24(numbers: number[]): Solution[] {
const solutions: Solution[] = [];
const seen = new Set<string>();
const perms = permutations(numbers);
for (const [a, b, c, d] of perms) {
for (const op1 of operators) {
for (const op2 of operators) {
for (const op3 of operators) {
// Evaluate all 5 patterns...
// (full implementation above)
}
}
}
}
return solutions.slice(0, 10); // Return top 10 solutions
}
The full solver runs in under 1ms for any input — brute force is perfectly fine here.
Tricky Examples
| Input | Solution |
|---|---|
| 1, 5, 5, 5 | 5 × (5 - 1/5) = 24 |
| 3, 3, 8, 8 | 8 / (3 - 8/3) = 24 |
| 1, 3, 4, 6 | 6 / (1 - 3/4) = 24 |
These are the puzzles that make people give up — and exactly why having a solver is satisfying.
Try It Live
I integrated this solver into my math puzzle platform. You can play the 24 Game with a built-in hint system that uses this exact algorithm:
👉 Play 24 Game on MathPuzzleHub
The solver powers the "Show Solution" button — it finds all valid expressions and displays them step by step.
Performance Notes
- Worst case (4 distinct numbers like 1, 2, 3, 4): ~7,680 evaluations, still under 1ms
- Best case (all same like 6, 6, 6, 6): only 1 unique permutation, 320 evaluations
- Memory: minimal — just storing up to 10 solution strings
If you wanted to scale this to 5 or 6 numbers, you'd want to switch from brute force to a recursive reduction approach (pick any two numbers, combine them, recurse on the smaller set). But for the classic 4-number game, brute force is elegant and fast.
Key Takeaways
- Brute force is underrated. 7,680 evaluations is nothing. Don't over-engineer.
- Five parenthesization patterns cover all binary expression trees for 4 operands — this comes from Catalan numbers.
-
Floating-point epsilon comparison is essential.
=== 24will miss valid solutions involving division. - Deduplication matters both for performance (fewer permutations) and UX (no duplicate solutions shown).
Happy solving! 🧮
Built with TypeScript and Next.js. Check out more math games at MathPuzzleHub.
Top comments (0)