Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

241. Different Ways to Add Parentheses - Leetcode Solution

Code Implementation

from typing import List

class Solution:
    def diffWaysToCompute(self, expression: str) -> List[int]:
        memo = {}

        def ways(expr):
            if expr in memo:
                return memo[expr]
            res = []
            for i, c in enumerate(expr):
                if c in "+-*":
                    left = ways(expr[:i])
                    right = ways(expr[i+1:])
                    for l in left:
                        for r in right:
                            if c == '+':
                                res.append(l + r)
                            elif c == '-':
                                res.append(l - r)
                            else:
                                res.append(l * r)
            if not res:
                res.append(int(expr))
            memo[expr] = res
            return res

        return ways(expression)
      
#include <vector>
#include <string>
#include <unordered_map>
using namespace std;

class Solution {
public:
    unordered_map<string, vector<int>> memo;
    vector<int> diffWaysToCompute(string expression) {
        if (memo.count(expression)) return memo[expression];
        vector<int> res;
        for (int i = 0; i < expression.size(); ++i) {
            char c = expression[i];
            if (c == '+' || c == '-' || c == '*') {
                vector<int> left = diffWaysToCompute(expression.substr(0, i));
                vector<int> right = diffWaysToCompute(expression.substr(i+1));
                for (int l : left) {
                    for (int r : right) {
                        if (c == '+') res.push_back(l + r);
                        else if (c == '-') res.push_back(l - r);
                        else res.push_back(l * r);
                    }
                }
            }
        }
        if (res.empty()) res.push_back(stoi(expression));
        memo[expression] = res;
        return res;
    }
};
      
import java.util.*;

class Solution {
    Map<String, List<Integer>> memo = new HashMap<>();
    public List<Integer> diffWaysToCompute(String expression) {
        if (memo.containsKey(expression)) return memo.get(expression);
        List<Integer> res = new ArrayList<>();
        for (int i = 0; i < expression.length(); i++) {
            char c = expression.charAt(i);
            if (c == '+' || c == '-' || c == '*') {
                List<Integer> left = diffWaysToCompute(expression.substring(0, i));
                List<Integer> right = diffWaysToCompute(expression.substring(i + 1));
                for (int l : left) {
                    for (int r : right) {
                        if (c == '+') res.add(l + r);
                        else if (c == '-') res.add(l - r);
                        else res.add(l * r);
                    }
                }
            }
        }
        if (res.isEmpty()) res.add(Integer.parseInt(expression));
        memo.put(expression, res);
        return res;
    }
}
      
/**
 * @param {string} expression
 * @return {number[]}
 */
var diffWaysToCompute = function(expression) {
    const memo = {};
    function ways(expr) {
        if (memo[expr]) return memo[expr];
        let res = [];
        for (let i = 0; i < expr.length; i++) {
            const c = expr[i];
            if (c === '+' || c === '-' || c === '*') {
                const left = ways(expr.slice(0, i));
                const right = ways(expr.slice(i + 1));
                for (const l of left) {
                    for (const r of right) {
                        if (c === '+') res.push(l + r);
                        else if (c === '-') res.push(l - r);
                        else res.push(l * r);
                    }
                }
            }
        }
        if (res.length === 0) res.push(Number(expr));
        memo[expr] = res;
        return res;
    }
    return ways(expression);
};
      

Problem Description

Given a string expression representing a valid mathematical expression containing numbers and the operators '+', '-', and '*', your task is to compute all possible results from computing all the different ways to group numbers and operators using parentheses.

Each grouping can change the order of operations, so you must consider all possible ways to add parentheses and evaluate the results for each grouping.

  • The input string expression contains only digits and the operators '+', '-', and '*'.
  • Numbers may have more than one digit.
  • You must return all possible results from computing all the different possible ways to group the numbers and operators with parentheses. Return the results in any order.

Thought Process

At first glance, the problem seems to ask us to evaluate a mathematical expression. However, the twist is that we are supposed to consider all possible ways to add parentheses and compute the result for each arrangement. This means we need to explore every way the expression could be split into sub-expressions, recursively.

A brute-force approach would be to try every possible split at every operator, recursively compute the results for the left and right parts, and then combine them according to the operator. However, since many sub-expressions may repeat, this can lead to redundant computations.

To optimize, we can use memoization to cache and reuse results of sub-expressions we've already computed. This helps avoid recomputation and speeds up the solution.

Solution Approach

  • Recursive Splitting: For every operator in the string, split the expression into two parts: left and right. Recursively compute all possible results for each part.
  • Combine Results: For each result from the left and right parts, combine them using the current operator, and store all possible outcomes.
  • Base Case: If the expression contains no operators, it must be a single number. Return it as the only result for that sub-expression.
  • Memoization: Use a hash map (dictionary) to cache the results for each sub-expression. If a sub-expression is encountered again, return its cached result instead of recomputing.
  • Return All Results: After processing all possible splits, return the list of all possible results for the current expression.

This approach ensures that we explore all valid parenthesizations and efficiently avoid redundant computations.

Example Walkthrough

Let's walk through the input expression = "2*3-4*5":

  1. We scan the expression and find operators at positions 1 ('*'), 3 ('-'), and 5 ('*').
  2. For each operator, we split and recursively solve the left and right sub-expressions:
    • Split at index 1 ('*'): Left is "2", Right is "3-4*5"
    • Split at index 3 ('-'): Left is "2*3", Right is "4*5"
    • Split at index 5 ('*'): Left is "2*3-4", Right is "5"
  3. For each split, recursively apply the same logic:
    • For "3-4*5": Split at '-' and '*', and so on.
  4. Combine all results using the operators, collecting all possible outcomes.
  5. The results for this example would be: [-34, -14, -10, -10, 10].

This process continues recursively, ensuring all possible groupings are considered.

Time and Space Complexity

  • Brute-force Approach: For an expression with n numbers, the number of ways to fully parenthesize (i.e., number of binary trees) is exponential (specifically, related to Catalan numbers). Thus, the time complexity is roughly O(2^n) in the worst case.
  • Optimized with Memoization: By caching results for each sub-expression, we avoid recomputation. The number of unique sub-expressions is O(n^2) (since each is defined by a start and end index), and each sub-expression may combine results in O(n) time. Thus, the overall time complexity is reduced to O(n^3).
  • Space Complexity: The memoization table stores O(n^2) entries, each potentially holding multiple results, so the space is also O(n^3) in the worst case.

Summary

The problem asks us to compute all possible results from different parenthesis groupings in a mathematical expression. By breaking the problem into smaller subproblems and using recursion with memoization, we efficiently explore all possible groupings and avoid redundant calculations. This recursive, divide-and-conquer strategy, enhanced by caching, makes the solution both elegant and efficient.