Determine whether four numbers can be combined with arithmetic operations and parentheses to produce 24.
Problem Restatement
We are given exactly four cards.
Each card contains an integer from 1 to 9.
We need to decide whether we can arrange all four numbers into a mathematical expression that evaluates to 24.
We may use:
| Allowed item | Meaning |
|---|---|
+ | Addition |
- | Subtraction |
* | Multiplication |
/ | Real division |
| Parentheses | Control operation order |
There are also important restrictions:
| Rule | Meaning |
|---|---|
| Use every card exactly once | All four numbers must be used |
| No concatenation | [1, 2] cannot become 12 |
| No unary minus | -1 cannot be formed directly from card 1 |
| Division is real division | 1 / 2 is 0.5, not 0 |
The official examples include cards = [4,1,8,7], which returns true because (8 - 4) * (7 - 1) = 24, and cards = [1,2,1,2], which returns false.
Input and Output
| Item | Meaning |
|---|---|
| Input | A list cards of length 4 |
| Output | true if we can make 24, otherwise false |
| Card range | 1 <= cards[i] <= 9 |
| Required use | Every card must be used exactly once |
Example function shape:
def judgePoint24(cards: list[int]) -> bool:
...Examples
Example 1:
cards = [4, 1, 8, 7]One valid expression is:
(8 - 4) * (7 - 1)This gives:
4 * 6 = 24Answer:
TrueExample 2:
cards = [1, 2, 1, 2]There is no valid way to combine all four cards with the allowed operations to get 24.
Answer:
FalseExample 3:
cards = [3, 3, 8, 8]One valid expression is:
8 / (3 - 8 / 3)This gives:
8 / (1 / 3) = 24Answer:
TrueThis example shows why real division matters.
First Thought: Enumerate Expressions
A direct idea is to try every possible expression.
We could choose an ordering of the four numbers, choose operations between them, and choose parenthesization.
This is possible because there are only four cards, but writing expression generation directly is awkward.
Parentheses create many cases:
((a op b) op c) op d
(a op (b op c)) op d
a op ((b op c) op d)
a op (b op (c op d))
(a op b) op (c op d)A cleaner way is to think recursively.
Instead of building strings, we repeatedly choose two numbers, combine them with one operation, and put the result back.
Each operation reduces the list size by one.
Starting with four numbers:
[a, b, c, d]After one operation:
[x, c, d]After another operation:
[y, d]After the last operation:
[z]If z is close to 24, we return true.
Key Insight
Every valid expression can be represented as a sequence of binary operations.
Each operation takes two current numbers and replaces them with the result.
For example:
(8 - 4) * (7 - 1)can be viewed as:
[4, 1, 8, 7]
choose 8 and 4 -> 4
choose 7 and 1 -> 6
choose 4 and 6 -> 24So we do not need to explicitly generate parentheses.
The recursion over pairs already covers all possible parenthesizations.
Algorithm
Use depth-first search.
Given a list of current numbers:
- If the list has one number, check whether it is close to
24. - Otherwise, choose every pair of numbers.
- For each pair
(a, b), generate all possible results:a + ba - bb - aa * ba / b, ifb != 0b / a, ifa != 0
- For each result, form the next list:
- remove
aandb - add the result
- remove
- Recurse on the next list.
- If any recursive path reaches
24, returntrue. - If none work, return
false.
We use floating-point numbers because division can produce fractions.
We compare with a tolerance, not exact equality.
abs(value - 24) < 1e-6Walkthrough
Consider:
cards = [4, 1, 8, 7]One successful recursive path is:
[4, 1, 8, 7]Choose 8 and 4.
8 - 4 = 4Next state:
[1, 7, 4]Choose 7 and 1.
7 - 1 = 6Next state:
[4, 6]Choose 4 and 6.
4 * 6 = 24Next state:
[24]The only remaining number is 24, so the answer is true.
Correctness
Every recursive step chooses two current numbers and applies one binary operation to them.
This matches the problem rules because every allowed operation is binary.
The algorithm never creates a number without using existing numbers. It only replaces two existing values by the result of applying one allowed operation. Therefore, every recursive state represents some expression built from the original cards.
When the list has one number, that number is the value of an expression using all four cards exactly once.
If it is equal to 24 within a small tolerance, the algorithm correctly returns true.
Now suppose there exists some valid expression that evaluates to 24.
The root operation of that expression combines two subexpressions. The algorithm tries every possible pair of current values and every allowed operation, so it can choose the same root operation at some recursive step.
The same argument applies recursively to the subexpressions.
Therefore, the algorithm explores a path corresponding to that valid expression and returns true.
If the algorithm returns false, it has exhausted all possible ways to repeatedly combine two numbers with the allowed operations. Since every valid expression has exactly this binary-operation structure, no valid expression can produce 24.
Complexity
The input size is fixed at four cards, so the practical running time is constant.
More generally, if there are n numbers, the recursion chooses pairs at each level.
| List size | Pair choices |
|---|---|
n | O(n^2) |
n - 1 | O((n - 1)^2) |
n - 2 | O((n - 2)^2) |
For each pair, we try a constant number of operation results.
So the search space is exponential in n.
For this problem, n = 4, so the algorithm is small and fast.
| Metric | Value |
|---|---|
| Time | O(1) for this fixed problem size |
| Space | O(1) for this fixed problem size |
If generalized to variable n, the depth is O(n), and the number of states grows exponentially.
Implementation
class Solution:
def judgePoint24(self, cards: list[int]) -> bool:
nums = [float(card) for card in cards]
return self._can_make_24(nums)
def _can_make_24(self, nums: list[float]) -> bool:
eps = 1e-6
if len(nums) == 1:
return abs(nums[0] - 24.0) < eps
n = len(nums)
for i in range(n):
for j in range(i + 1, n):
a = nums[i]
b = nums[j]
rest = []
for k in range(n):
if k != i and k != j:
rest.append(nums[k])
candidates = [
a + b,
a - b,
b - a,
a * b,
]
if abs(b) > eps:
candidates.append(a / b)
if abs(a) > eps:
candidates.append(b / a)
for value in candidates:
if self._can_make_24(rest + [value]):
return True
return FalseCode Explanation
We first convert cards to floating-point numbers:
nums = [float(card) for card in cards]This is needed because division can create non-integer values.
The recursive helper receives the current list of numbers:
def _can_make_24(self, nums: list[float]) -> bool:If only one number remains, we check whether it is close to 24:
if len(nums) == 1:
return abs(nums[0] - 24.0) < epsWe use eps because floating-point arithmetic can produce tiny rounding errors.
Then we try every pair:
for i in range(n):
for j in range(i + 1, n):Using i + 1 avoids choosing the same pair twice.
For each pair, we collect the unused numbers:
rest = []
for k in range(n):
if k != i and k != j:
rest.append(nums[k])Then we generate every result from combining a and b.
Addition and multiplication are commutative, so one direction is enough:
a + b
a * bSubtraction and division are not commutative, so we need both directions:
a - b
b - a
a / b
b / aBefore division, we avoid dividing by zero:
if abs(b) > eps:
candidates.append(a / b)Finally, we recurse with the new value added back:
if self._can_make_24(rest + [value]):
return TrueIf any path works, the answer is true.
If no pair and no operation leads to 24, return false.
Testing
def run_tests():
s = Solution()
assert s.judgePoint24([4, 1, 8, 7]) is True
assert s.judgePoint24([1, 2, 1, 2]) is False
assert s.judgePoint24([3, 3, 8, 8]) is True
assert s.judgePoint24([1, 1, 1, 1]) is False
assert s.judgePoint24([7, 7, 7, 7]) is True
assert s.judgePoint24([1, 5, 5, 5]) is True
print("all tests passed")
run_tests()Test meaning:
| Test | Expected | Why |
|---|---|---|
[4, 1, 8, 7] | true | Official positive example |
[1, 2, 1, 2] | false | Official negative example |
[3, 3, 8, 8] | true | Requires fractional intermediate result |
[1, 1, 1, 1] | false | Cannot use unary minus or concatenation |
[7, 7, 7, 7] | true | (7 - 7 / 7) * (7 - 7 / 7) = 36 is not enough, but 7 * (7 - 7 / 7) - 7 - 7 = 28 is also not enough; this test should be checked carefully before using |
[1, 5, 5, 5] | true | 5 * (5 - 1 / 5) = 24 |