Facebook Pixel

679. 24 Game

Problem Description

You are given an array of 4 integers called cards, where each integer is between 1 and 9 (inclusive). Your task is to determine if you can arrange these four numbers using mathematical operations and parentheses to create an expression that evaluates to exactly 24.

You can use the following operators between any two numbers:

  • Addition +
  • Subtraction -
  • Multiplication *
  • Division /

You can also use parentheses ( and ) to control the order of operations.

However, there are several important restrictions:

  1. Real Division: The division operator / represents real division (not integer division). For example, 4 / (1 - 2/3) = 4 / (1/3) = 12.

  2. Binary Operations Only: Every operation must be between exactly two numbers. You cannot use - as a unary operator. For instance, if cards = [1, 1, 1, 1], the expression "-1 - 1 - 1 - 1" is not allowed.

  3. No Number Concatenation: You cannot combine digits to form multi-digit numbers. For example, if cards = [1, 2, 1, 2], you cannot create the expression "12 + 12".

The function should return true if it's possible to create an expression using all four cards exactly once that evaluates to 24, and false otherwise.

For example:

  • If cards = [4, 1, 8, 7], you could form (8 - 4) * (7 - 1) = 4 * 6 = 24, so return true
  • If cards = [1, 2, 1, 2], despite various attempts, you cannot form 24, so return false

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • No: The problem doesn't involve nodes and edges or graph traversal. We're working with 4 numbers and mathematical operations.

Need to solve for kth smallest/largest?

  • No: We're not looking for the kth smallest or largest value. We need to determine if we can make exactly 24.

Involves Linked Lists?

  • No: The problem deals with an array of 4 integers, not linked list operations.

Does the problem have small constraints?

  • Yes: The constraint is very small - we only have 4 cards. With 4 numbers, we need to try different combinations of operators and orderings, which is a manageable search space.

Brute force / Backtracking?

  • Yes: Given the small constraints (only 4 numbers), we can use brute force/backtracking to:
    • Try all possible ways to pick 2 numbers from the current set
    • Apply all 4 operators (+, -, *, /) between them
    • Replace the two numbers with their result
    • Recursively continue until we have just 1 number left
    • Check if that final number equals 24

Conclusion: The flowchart correctly leads us to use a Backtracking approach. The small problem size (4 cards) makes it feasible to exhaustively try all possible combinations of operations and orderings. We recursively reduce the problem from 4 numbers to 3, then to 2, then to 1, backtracking when a path doesn't lead to 24.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that with only 4 numbers, we can think of this problem as repeatedly combining two numbers until we're left with just one final result.

Think about how you would solve this manually: you'd pick any two numbers, apply an operation between them, and get a result. Now you have 3 numbers instead of 4. You'd repeat this process - pick two from these three, apply an operation, and now you have 2 numbers. Finally, apply one last operation to get a single number, and check if it equals 24.

For example, with [4, 1, 8, 7]:

  • First, we might combine 8 and 4 with subtraction: 8 - 4 = 4, leaving us with [4, 1, 7]
  • Then combine 7 and 1 with subtraction: 7 - 1 = 6, leaving us with [4, 6]
  • Finally combine 4 and 6 with multiplication: 4 * 6 = 24

The beauty of this approach is that it naturally handles parentheses. When we combine two numbers first, it's like putting parentheses around them. The order in which we choose pairs and operations implicitly creates different parenthesization patterns.

Since we need to try all possible ways of combining numbers and all possible operators, this is a perfect fit for backtracking:

  1. At each step, try all possible pairs of numbers from our current list
  2. For each pair, try all four operators
  3. Replace the pair with their result and recursively solve the smaller problem
  4. If we ever reach a single number that equals 24, we've found a solution
  5. If not, backtrack and try a different combination

The reason we use floating-point numbers and check with a small epsilon (1e-6) rather than exact equality is to handle division operations correctly, since division can produce non-integer results that might lead to 24 through further operations.

Learn more about Math and Backtracking patterns.

Solution Approach

The solution uses a recursive DFS (Depth-First Search) with backtracking to explore all possible ways of combining the four numbers.

Core Algorithm Structure:

The main function dfs(nums) takes a list of floating-point numbers and returns whether we can reach 24 by combining them.

Base Case: When nums has only 1 element left, we check if it's approximately equal to 24 (using abs(nums[0] - 24) < 1e-6 to handle floating-point precision issues). This is our termination condition.

Recursive Case: For each recursive call with n numbers:

  1. Select Two Numbers: We use nested loops to pick any two different numbers from the current list:

    for i in range(n):
        for j in range(n):
            if i != j:

    This gives us all ordered pairs (nums[i], nums[j]) where i ≠ j.

  2. Create New Number List: For each pair selected, we create a new list containing:

    • All numbers except the two we selected
    • Plus the result of applying an operation to our pair
    nxt = [nums[k] for k in range(n) if k != i and k != j]
  3. Try All Operations: For each pair, we try all four operations:

    • Addition: nums[i] + nums[j]
    • Subtraction: nums[i] - nums[j]
    • Multiplication: nums[i] * nums[j]
    • Division: nums[i] / nums[j] (with zero-check for nums[j])
  4. Recursive Call: After applying each operation, we recursively call dfs with the new list (original numbers minus the pair, plus their result):

    ok |= dfs(nxt + [result])
  5. Early Termination: If any recursive path returns True, we immediately return True (successful backtracking).

Key Implementation Details:

  • Floating-Point Conversion: We convert all integers to floats at the start to handle division properly.
  • Division by Zero Check: Before division, we check if the divisor is zero to avoid runtime errors.
  • Order Matters: Since subtraction and division are non-commutative, trying both (i, j) and (j, i) gives us both a - b and b - a, as well as a / b and b / a.
  • Using |= operator: The code uses ok |= dfs(...) to accumulate results, though it could return immediately upon finding True for efficiency.

This approach effectively tries all possible binary expression trees with 4 leaves (our numbers) and 3 internal nodes (operations), exploring the entire solution space through backtracking.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through the solution with cards = [4, 1, 8, 7] to see how the algorithm finds that we can make 24.

Initial State: nums = [4.0, 1.0, 8.0, 7.0] (4 numbers)

First Recursion Level: The algorithm tries different pairs. Let's follow one successful path:

  • Select pair: 8.0 (index 2) and 4.0 (index 0)
  • Try subtraction: 8.0 - 4.0 = 4.0
  • Create new list: [1.0, 7.0, 4.0] (removed 8.0 and 4.0, added their difference)
  • Recursively call dfs([1.0, 7.0, 4.0])

Second Recursion Level: nums = [1.0, 7.0, 4.0] (3 numbers)

  • Select pair: 7.0 (index 1) and 1.0 (index 0)
  • Try subtraction: 7.0 - 1.0 = 6.0
  • Create new list: [4.0, 6.0] (removed 7.0 and 1.0, added their difference)
  • Recursively call dfs([4.0, 6.0])

Third Recursion Level: nums = [4.0, 6.0] (2 numbers)

  • Select pair: 4.0 (index 0) and 6.0 (index 1)
  • Try multiplication: 4.0 * 6.0 = 24.0
  • Create new list: [24.0] (removed 4.0 and 6.0, added their product)
  • Recursively call dfs([24.0])

Base Case: nums = [24.0] (1 number)

  • Check: abs(24.0 - 24) < 1e-6True!
  • Return True

The algorithm backtracks with True, confirming that [4, 1, 8, 7] can make 24.

This path corresponds to the expression (8 - 4) * (7 - 1) = 24. Note how:

  • The first combination (8 - 4) created an implicit grouping
  • The second combination (7 - 1) created another grouping
  • The final multiplication combined these two grouped results
  • The algorithm naturally explored this parenthesization without explicitly handling parentheses

If this path hadn't worked, the algorithm would backtrack and try other combinations (different pairs, different operations) until either finding a solution or exhausting all possibilities.

Solution Implementation

1class Solution:
2    def judgePoint24(self, cards: List[int]) -> bool:
3        """
4        Determine if four cards can be combined using +, -, *, / to reach 24.
5      
6        Args:
7            cards: List of 4 integers representing card values
8          
9        Returns:
10            True if the cards can form 24, False otherwise
11        """
12      
13        def can_reach_target(numbers: List[float]) -> bool:
14            """
15            Recursively try all combinations of operations on remaining numbers.
16          
17            Args:
18                numbers: Current list of numbers to work with
19              
20            Returns:
21                True if we can reach 24 with these numbers
22            """
23            # Base case: if only one number left, check if it equals 24
24            if len(numbers) == 1:
25                # Use epsilon comparison for floating point equality
26                return abs(numbers[0] - 24) < 1e-6
27          
28            # Try all pairs of numbers
29            for first_idx in range(len(numbers)):
30                for second_idx in range(len(numbers)):
31                    # Skip if same number (can't combine a number with itself)
32                    if first_idx == second_idx:
33                        continue
34                  
35                    # Create new list excluding the two selected numbers
36                    remaining_numbers = [
37                        numbers[k] 
38                        for k in range(len(numbers)) 
39                        if k != first_idx and k != second_idx
40                    ]
41                  
42                    # Try all four operations
43                    for operation in OPERATIONS:
44                        if operation == "+":
45                            # Addition: a + b
46                            result = numbers[first_idx] + numbers[second_idx]
47                        elif operation == "-":
48                            # Subtraction: a - b
49                            result = numbers[first_idx] - numbers[second_idx]
50                        elif operation == "*":
51                            # Multiplication: a * b
52                            result = numbers[first_idx] * numbers[second_idx]
53                        elif operation == "/":
54                            # Division: a / b (check for division by zero)
55                            if numbers[second_idx] == 0:
56                                continue
57                            result = numbers[first_idx] / numbers[second_idx]
58                      
59                        # Recursively check if we can reach 24 with the new set of numbers
60                        if can_reach_target(remaining_numbers + [result]):
61                            return True
62          
63            # No valid combination found
64            return False
65      
66        # Define available operations
67        OPERATIONS = ("+", "-", "*", "/")
68      
69        # Convert integers to floats for accurate division
70        float_numbers = [float(card) for card in cards]
71      
72        # Start the recursive search
73        return can_reach_target(float_numbers)
74
1class Solution {
2    // Define available operations
3    private final char[] operations = {'+', '-', '*', '/'};
4    // Epsilon for floating point comparison
5    private static final double EPSILON = 1e-6;
6  
7    /**
8     * Determines if four cards can make 24 using basic arithmetic operations
9     * @param cards array of 4 integers representing card values
10     * @return true if the cards can make 24, false otherwise
11     */
12    public boolean judgePoint24(int[] cards) {
13        // Convert integer cards to double list for precision in division
14        List<Double> numbers = new ArrayList<>();
15        for (int card : cards) {
16            numbers.add((double) card);
17        }
18        return backtrack(numbers);
19    }
20  
21    /**
22     * Recursively tries all possible combinations of operations on pairs of numbers
23     * @param numbers current list of numbers to work with
24     * @return true if current combination can reach 24
25     */
26    private boolean backtrack(List<Double> numbers) {
27        int size = numbers.size();
28      
29        // Base case: if only one number left, check if it equals 24
30        if (size == 1) {
31            return Math.abs(numbers.get(0) - 24) < EPSILON;
32        }
33      
34        // Try all pairs of numbers
35        for (int i = 0; i < size; i++) {
36            for (int j = 0; j < size; j++) {
37                // Skip if same index
38                if (i == j) {
39                    continue;
40                }
41              
42                // Create new list with remaining numbers (excluding i and j)
43                List<Double> nextNumbers = new ArrayList<>();
44                for (int k = 0; k < size; k++) {
45                    if (k != i && k != j) {
46                        nextNumbers.add(numbers.get(k));
47                    }
48                }
49              
50                // Try all operations on the selected pair
51                for (char operation : operations) {
52                    // Calculate result based on operation
53                    double result = 0;
54                    boolean validOperation = true;
55                  
56                    switch (operation) {
57                        case '+':
58                            result = numbers.get(i) + numbers.get(j);
59                            break;
60                        case '-':
61                            result = numbers.get(i) - numbers.get(j);
62                            break;
63                        case '*':
64                            result = numbers.get(i) * numbers.get(j);
65                            break;
66                        case '/':
67                            // Check for division by zero
68                            if (Math.abs(numbers.get(j)) < EPSILON) {
69                                validOperation = false;
70                            } else {
71                                result = numbers.get(i) / numbers.get(j);
72                            }
73                            break;
74                    }
75                  
76                    // Skip invalid operations
77                    if (!validOperation) {
78                        continue;
79                    }
80                  
81                    // Add the result to the next list
82                    nextNumbers.add(result);
83                  
84                    // Recursively check if this path leads to 24
85                    if (backtrack(nextNumbers)) {
86                        return true;
87                    }
88                  
89                    // Backtrack: remove the added result for next iteration
90                    nextNumbers.remove(nextNumbers.size() - 1);
91                }
92            }
93        }
94      
95        return false;
96    }
97}
98
1class Solution {
2public:
3    bool judgePoint24(vector<int>& cards) {
4        // Convert integer cards to double for precise calculation
5        vector<double> numbers;
6        for (int card : cards) {
7            numbers.push_back(static_cast<double>(card));
8        }
9        return canMake24(numbers);
10    }
11
12private:
13    // Epsilon for floating point comparison
14    static constexpr double EPSILON = 1e-6;
15  
16    // Target value to reach
17    static constexpr double TARGET = 24.0;
18  
19    // Available arithmetic operations
20    static constexpr char OPERATIONS[4] = {'+', '-', '*', '/'};
21
22    bool canMake24(vector<double>& numbers) {
23        int size = numbers.size();
24      
25        // Base case: if only one number left, check if it equals 24
26        if (size == 1) {
27            return abs(numbers[0] - TARGET) < EPSILON;
28        }
29      
30        // Try all possible pairs of numbers
31        for (int i = 0; i < size; ++i) {
32            for (int j = 0; j < size; ++j) {
33                // Skip if selecting the same number
34                if (i == j) {
35                    continue;
36                }
37              
38                // Create new vector with remaining numbers (excluding i and j)
39                vector<double> remainingNumbers;
40                for (int k = 0; k < size; ++k) {
41                    if (k != i && k != j) {
42                        remainingNumbers.push_back(numbers[k]);
43                    }
44                }
45              
46                // Try all four operations on the selected pair
47                for (char operation : OPERATIONS) {
48                    // Calculate result based on operation
49                    double result = 0.0;
50                    bool validOperation = true;
51                  
52                    switch (operation) {
53                        case '+':
54                            result = numbers[i] + numbers[j];
55                            break;
56                        case '-':
57                            result = numbers[i] - numbers[j];
58                            break;
59                        case '*':
60                            result = numbers[i] * numbers[j];
61                            break;
62                        case '/':
63                            // Skip division by zero
64                            if (abs(numbers[j]) < EPSILON) {
65                                validOperation = false;
66                            } else {
67                                result = numbers[i] / numbers[j];
68                            }
69                            break;
70                    }
71                  
72                    // Skip invalid operations
73                    if (!validOperation) {
74                        continue;
75                    }
76                  
77                    // Add the result to remaining numbers and recurse
78                    remainingNumbers.push_back(result);
79                  
80                    // If found a valid solution, return immediately
81                    if (canMake24(remainingNumbers)) {
82                        return true;
83                    }
84                  
85                    // Backtrack: remove the result for next iteration
86                    remainingNumbers.pop_back();
87                }
88            }
89        }
90      
91        // No valid combination found
92        return false;
93    }
94};
95
1/**
2 * Determines if four cards can be combined using +, -, *, / to get 24
3 * @param cards - Array of 4 numbers representing card values
4 * @returns true if the cards can make 24, false otherwise
5 */
6function judgePoint24(cards: number[]): boolean {
7    // Define the four basic arithmetic operations
8    const operations: string[] = ['+', '-', '*', '/'];
9  
10    /**
11     * Recursive DFS function to try all possible combinations of numbers and operations
12     * @param numbers - Current array of numbers to work with
13     * @returns true if current combination can reach 24
14     */
15    const depthFirstSearch = (numbers: number[]): boolean => {
16        const count: number = numbers.length;
17      
18        // Base case: if only one number left, check if it equals 24
19        if (count === 1) {
20            // Use epsilon comparison for floating point precision
21            return Math.abs(numbers[0] - 24) < 1e-6;
22        }
23      
24        let found: boolean = false;
25      
26        // Try all pairs of numbers
27        for (let firstIndex = 0; firstIndex < count; firstIndex++) {
28            for (let secondIndex = 0; secondIndex < count; secondIndex++) {
29                // Skip if same number (can't combine a number with itself)
30                if (firstIndex !== secondIndex) {
31                    // Create new array with remaining numbers (excluding the pair)
32                    const nextNumbers: number[] = [];
33                    for (let k = 0; k < count; k++) {
34                        if (k !== firstIndex && k !== secondIndex) {
35                            nextNumbers.push(numbers[k]);
36                        }
37                    }
38                  
39                    // Try all four operations on the selected pair
40                    for (const operation of operations) {
41                        // Calculate result based on operation
42                        switch (operation) {
43                            case '/':
44                                // Avoid division by zero
45                                if (numbers[secondIndex] === 0) {
46                                    continue;
47                                }
48                                nextNumbers.push(numbers[firstIndex] / numbers[secondIndex]);
49                                break;
50                            case '*':
51                                nextNumbers.push(numbers[firstIndex] * numbers[secondIndex]);
52                                break;
53                            case '+':
54                                nextNumbers.push(numbers[firstIndex] + numbers[secondIndex]);
55                                break;
56                            case '-':
57                                nextNumbers.push(numbers[firstIndex] - numbers[secondIndex]);
58                                break;
59                        }
60                      
61                        // Recursively check if this path leads to 24
62                        found = found || depthFirstSearch(nextNumbers);
63                      
64                        // Early return if solution found
65                        if (found) {
66                            return true;
67                        }
68                      
69                        // Backtrack: remove the result to try next operation
70                        nextNumbers.pop();
71                    }
72                }
73            }
74        }
75      
76        return found;
77    };
78  
79    // Start the recursive search with the initial cards
80    return depthFirstSearch(cards);
81}
82

Time and Space Complexity

Time Complexity: O(1) (constant time with upper bound)

The algorithm explores all possible ways to combine 4 numbers using binary operations. At each recursive step with n numbers:

  • We choose 2 numbers from n numbers: n * (n-1) ordered pairs
  • We apply 4 operations to each pair
  • We recursively process n-1 numbers

The recurrence relation is: T(n) = n * (n-1) * 4 * T(n-1) with T(1) = 1

Solving this:

  • T(4) = 4 * 3 * 4 * T(3)
  • T(3) = 3 * 2 * 4 * T(2)
  • T(2) = 2 * 1 * 4 * T(1)
  • T(1) = 1

Therefore: T(4) = 4 * 3 * 4 * 3 * 2 * 4 * 2 * 1 * 4 * 1 = 12 * 24 * 32 = 9216

Since the input is always exactly 4 cards, this is a constant upper bound, making the complexity O(1).

Space Complexity: O(1)

The recursion depth is at most 3 (from 4 numbers → 3 numbers → 2 numbers → 1 number). At each level, we create a new list nxt containing at most 3 elements. The space used per recursive call is O(n) where n ≤ 4. Since the maximum recursion depth is 3 and each level uses constant space, the total space complexity is O(1).

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Inefficient Operation Redundancy

The current implementation tries all ordered pairs and all operations, which leads to redundant calculations. For example:

  • When trying addition or multiplication (commutative operations), calculating both a + b and b + a produces the same result
  • The algorithm explores both (i, j) and (j, i) pairs unnecessarily for commutative operations

Solution: Optimize by recognizing commutative properties:

def can_reach_target(numbers: List[float]) -> bool:
    if len(numbers) == 1:
        return abs(numbers[0] - 24) < 1e-6
  
    n = len(numbers)
    # Use i < j to avoid duplicate pairs
    for i in range(n):
        for j in range(i + 1, n):  # Start from i+1 instead of 0
            a, b = numbers[i], numbers[j]
            remaining = [numbers[k] for k in range(n) if k != i and k != j]
          
            # For commutative operations, try once
            for result in [a + b, a * b]:
                if can_reach_target(remaining + [result]):
                    return True
          
            # For non-commutative operations, try both orders
            for result in [a - b, b - a]:
                if can_reach_target(remaining + [result]):
                    return True
          
            # Division with zero checks
            if b != 0 and can_reach_target(remaining + [a / b]):
                return True
            if a != 0 and can_reach_target(remaining + [b / a]):
                return True
  
    return False

2. Floating-Point Precision Issues

While the code uses 1e-6 for epsilon comparison, this threshold might be too strict or too lenient depending on the operations performed. Multiple divisions and multiplications can accumulate rounding errors.

Solution: Use a more robust epsilon value or implement exact fraction arithmetic:

from fractions import Fraction

def judgePoint24(self, cards: List[int]) -> bool:
    def can_reach_target(numbers: List[Fraction]) -> bool:
        if len(numbers) == 1:
            return numbers[0] == 24  # Exact comparison with fractions
      
        # ... rest of the logic using Fraction arithmetic
      
    # Convert to fractions for exact arithmetic
    fraction_numbers = [Fraction(card) for card in cards]
    return can_reach_target(fraction_numbers)

3. Missing Early Exit Optimization

The current code continues checking even after finding a valid solution in some branches, using |= operator without immediate return.

Solution: Return immediately upon finding a valid path:

for operation in OPERATIONS:
    # ... calculate result ...
    if can_reach_target(remaining_numbers + [result]):
        return True  # Exit immediately

4. Lack of Memoization

The algorithm might revisit the same set of numbers multiple times through different paths (e.g., [2, 3, 4] could be reached via 1+1=2 or 3-1=2 from different starting combinations).

Solution: Add memoization using frozenset for unordered comparison:

def judgePoint24(self, cards: List[int]) -> bool:
    memo = {}
  
    def can_reach_target(numbers: tuple) -> bool:
        # Create a key that's order-independent
        key = tuple(sorted(numbers))
        if key in memo:
            return memo[key]
      
        if len(numbers) == 1:
            result = abs(numbers[0] - 24) < 1e-6
            memo[key] = result
            return result
      
        # ... rest of the logic ...
      
        memo[key] = False  # Cache negative result
        return False
  
    return can_reach_target(tuple(float(card) for card in cards))

These optimizations can significantly improve both the performance and reliability of the solution.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What's the output of running the following function using the following tree as input?

1def serialize(root):
2    res = []
3    def dfs(root):
4        if not root:
5            res.append('x')
6            return
7        res.append(root.val)
8        dfs(root.left)
9        dfs(root.right)
10    dfs(root)
11    return ' '.join(res)
12
1import java.util.StringJoiner;
2
3public static String serialize(Node root) {
4    StringJoiner res = new StringJoiner(" ");
5    serializeDFS(root, res);
6    return res.toString();
7}
8
9private static void serializeDFS(Node root, StringJoiner result) {
10    if (root == null) {
11        result.add("x");
12        return;
13    }
14    result.add(Integer.toString(root.val));
15    serializeDFS(root.left, result);
16    serializeDFS(root.right, result);
17}
18
1function serialize(root) {
2    let res = [];
3    serialize_dfs(root, res);
4    return res.join(" ");
5}
6
7function serialize_dfs(root, res) {
8    if (!root) {
9        res.push("x");
10        return;
11    }
12    res.push(root.val);
13    serialize_dfs(root.left, res);
14    serialize_dfs(root.right, res);
15}
16

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More