Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

679. 24 Game - Leetcode Solution

Code Implementation

class Solution:
    def judgePoint24(self, nums):
        def dfs(nums):
            if len(nums) == 1:
                return abs(nums[0] - 24) < 1e-6
            for i in range(len(nums)):
                for j in range(len(nums)):
                    if i == j:
                        continue
                    n1, n2 = nums[i], nums[j]
                    next_nums = [nums[k] for k in range(len(nums)) if k != i and k != j]
                    for op in ['+', '-', '*', '/']:
                        if op == '+':
                            res = n1 + n2
                        elif op == '-':
                            res = n1 - n2
                        elif op == '*':
                            res = n1 * n2
                        elif op == '/':
                            if abs(n2) < 1e-6:
                                continue
                            res = n1 / n2
                        if dfs(next_nums + [res]):
                            return True
            return False
        return dfs([float(x) for x in nums])
      
class Solution {
public:
    bool judgePoint24(vector<int>& nums) {
        vector<double> arr(nums.begin(), nums.end());
        return dfs(arr);
    }
    bool dfs(vector<double>& nums) {
        if (nums.size() == 1)
            return fabs(nums[0] - 24) < 1e-6;
        for (int i = 0; i < nums.size(); ++i) {
            for (int j = 0; j < nums.size(); ++j) {
                if (i == j) continue;
                vector<double> next;
                for (int k = 0; k < nums.size(); ++k)
                    if (k != i && k != j) next.push_back(nums[k]);
                for (int op = 0; op < 4; ++op) {
                    if (op < 2 && i > j) continue; // avoid duplicate
                    double res = 0.0;
                    if (op == 0) res = nums[i] + nums[j];
                    if (op == 1) res = nums[i] * nums[j];
                    if (op == 2) res = nums[i] - nums[j];
                    if (op == 3) {
                        if (fabs(nums[j]) < 1e-6) continue;
                        res = nums[i] / nums[j];
                    }
                    next.push_back(res);
                    if (dfs(next)) return true;
                    next.pop_back();
                }
            }
        }
        return false;
    }
};
      
class Solution {
    public boolean judgePoint24(int[] nums) {
        List<Double> list = new ArrayList<>();
        for (int n : nums) list.add((double) n);
        return dfs(list);
    }
    private boolean dfs(List<Double> nums) {
        if (nums.size() == 1)
            return Math.abs(nums.get(0) - 24) < 1e-6;
        for (int i = 0; i < nums.size(); i++) {
            for (int j = 0; j < nums.size(); j++) {
                if (i == j) continue;
                List<Double> next = new ArrayList<>();
                for (int k = 0; k < nums.size(); k++)
                    if (k != i && k != j) next.add(nums.get(k));
                for (int op = 0; op < 4; op++) {
                    if (op < 2 && i > j) continue;
                    double res = 0;
                    if (op == 0) res = nums.get(i) + nums.get(j);
                    if (op == 1) res = nums.get(i) * nums.get(j);
                    if (op == 2) res = nums.get(i) - nums.get(j);
                    if (op == 3) {
                        if (Math.abs(nums.get(j)) < 1e-6) continue;
                        res = nums.get(i) / nums.get(j);
                    }
                    next.add(res);
                    if (dfs(next)) return true;
                    next.remove(next.size() - 1);
                }
            }
        }
        return false;
    }
}
      
var judgePoint24 = function(nums) {
    function dfs(arr) {
        if (arr.length === 1) {
            return Math.abs(arr[0] - 24) < 1e-6;
        }
        for (let i = 0; i < arr.length; i++) {
            for (let j = 0; j < arr.length; j++) {
                if (i === j) continue;
                let next = [];
                for (let k = 0; k < arr.length; k++) {
                    if (k !== i && k !== j) next.push(arr[k]);
                }
                for (let op of ['+', '-', '*', '/']) {
                    if (op === '+') {
                        next.push(arr[i] + arr[j]);
                    } else if (op === '-') {
                        next.push(arr[i] - arr[j]);
                    } else if (op === '*') {
                        next.push(arr[i] * arr[j]);
                    } else if (op === '/') {
                        if (Math.abs(arr[j]) < 1e-6) continue;
                        next.push(arr[i] / arr[j]);
                    }
                    if (dfs(next)) return true;
                    next.pop();
                }
            }
        }
        return false;
    }
    return dfs(nums.map(x => x * 1.0));
};
      

Problem Description

Given an array nums of exactly four integers, you must determine if you can use each number exactly once, along with any combination of the four basic arithmetic operations (+, -, *, /) and parentheses, to reach the number 24.

  • Each number must be used exactly once.
  • You can use any order and grouping of operations.
  • You cannot concatenate numbers or reuse any number.
  • You may assume division by zero does not occur in your solution.
  • Return true if it is possible to reach 24, otherwise false.

Thought Process

At first glance, this problem seems like a brute-force search: try every possible combination of numbers, operations, and order of operations. However, there are many possible ways to combine four numbers and operations, especially since parentheses can dramatically change the result.

The main challenge is to systematically consider all possible ways to combine the numbers with operations, while ensuring each number is used only once and all possible groupings (i.e., with parentheses) are considered.

We quickly realize that brute-forcing all permutations and operations is feasible because there are only four numbers. But to avoid redundant calculations and make the code manageable, we need a recursive approach that, at each step, combines two numbers with an operation and recurses on the resulting list.

Floating-point precision is another concern: some results may be very close to 24 but not exactly 24 due to division, so we must check if the result is "close enough".

Solution Approach

  • Recursive Reduction: The key insight is to recursively reduce the list of numbers by combining any two numbers with any operation, then recurse on the new list (with one fewer number).
  • Base Case: If the list has only one number left, check if it is (approximately) 24.
  • Try All Pairs and Operations: For each pair of numbers, try all four operations. For non-commutative operations (- and /), try both orders.
  • Floating-Point Comparison: Because division can lead to floating-point errors, compare the result to 24 within a small epsilon (e.g., 1e-6).
  • Pruning: If at any point a division by zero would occur, skip that operation.
  • Return Early: As soon as any recursive branch returns true, propagate that result up.

This approach ensures all possible groupings and operation orders are explored, but avoids redundant work by not reusing numbers or repeating the same pair in both orders for commutative operations.

Example Walkthrough

Let's use nums = [4, 1, 8, 7] as an example.

  1. Pick any two numbers, say 8 and 4, and try all operations:
    • 8 + 4 = 12
    • 8 - 4 = 4
    • 4 - 8 = -4
    • 8 * 4 = 32
    • 8 / 4 = 2
    • 4 / 8 = 0.5
  2. For each result, create a new list with the result and the remaining numbers (1, 7), and repeat the process.
  3. Eventually, one sequence leads to:
    • 8 - 4 = 4
    • Now have [4, 1, 7]
    • Try 7 * (4 + 1) = 7 * 5 = 35
    • Try (7 + 1) * 4 = 8 * 4 = 32
    • Try (7 - 1) * 4 = 6 * 4 = 24
  4. When we find 24, the function returns true.
  5. If no sequence leads to 24, the function returns false.

The recursive exploration ensures all combinations and groupings are checked.

Time and Space Complexity

Brute-Force Approach:

  • There are 4 numbers, and at each step, we choose 2 out of n numbers to combine, try 4 operations, and recurse. The number of recursive calls is bounded (since n is small), but the total number of possible expressions is still significant.
  • For each pair, there are O(n^2) choices, and 4 operations, so the total number of possibilities is roughly O(4^n * n!).
  • For n=4, this is manageable (a few thousand recursive calls).
Time Complexity: O(1) for practical purposes (since n is fixed at 4), but the theoretical bound is O(4^n * n!).
Space Complexity: O(n) for the recursion stack, which is at most 4 levels deep.

Summary

The 24 Game problem is a classic example of recursive backtracking. By systematically combining pairs of numbers with all possible operations and recursing, we can check if any sequence of operations produces 24. Key insights include handling floating-point precision, pruning impossible branches early, and realizing that the problem size is small enough to allow a brute-force recursive solution. This approach is elegant in its simplicity and exhaustiveness, ensuring all valid expressions are considered.