Different Ways to Add Parentheses: Divide and Conquer Expressions
Given a string expression containing numbers and the operators +, -, and *, return all possible results from computing every different way to group the numbers and operators. You may return the results in any order.
This is LeetCode 241: Different Ways to Add Parentheses. For example, given "2*3-4*5", there are multiple ways to place parentheses: ((2*3)-(4*5)) evaluates to -14, while (2*(3-(4*5))) gives -34. Your job is to find every possible evaluation.
The key insight is that every valid grouping splits the expression at exactly one operator at the top level. That operator becomes the "root" of the evaluation, and the left and right sides become independent sub-problems. This naturally leads to a divide and conquer approach.
The divide and conquer idea
Think about what happens when you choose a single operator as the "last" operation to evaluate. Everything to the left of that operator must be fully evaluated first, and everything to the right must be fully evaluated first. Then you combine the two results using that operator.
Since the left and right halves can themselves be parenthesized in multiple ways, each half may produce multiple possible values. You need to combine every possible left result with every possible right result.
The algorithm becomes:
- Scan through the expression looking for operators (
+,-,*) - For each operator found, split the expression into a left part and a right part
- Recursively compute all possible results for the left part
- Recursively compute all possible results for the right part
- Combine every left result with every right result using the operator
- If no operator was found, the expression is a single number, so return it directly
The base case is crucial here. When the expression has no operators, it is just a number. The recursive calls naturally bottom out at single numbers, and the combinations bubble up to produce all possible evaluations.
Python solution
def diff_ways_to_compute(expression):
results = []
for i, char in enumerate(expression):
if char in "+-*":
left = diff_ways_to_compute(expression[:i])
right = diff_ways_to_compute(expression[i + 1:])
for l in left:
for r in right:
if char == '+':
results.append(l + r)
elif char == '-':
results.append(l - r)
else:
results.append(l * r)
if not results:
results.append(int(expression))
return results
The function iterates over every character in the expression. When it finds an operator, it splits the string into two halves, recursively solves both, and combines all pairs. If after scanning the entire expression no operator was found, that means the expression is a pure number, so it gets converted to an integer and returned as the sole result.
Step-by-step walkthrough
Let's trace through "2*3-4*5" to see how the recursion unfolds:
Step 1: Identify operators in "2*3-4*5"
Three operators found: * at index 1, - at index 3, * at index 5. Each one is a potential split point. We will recurse on all three splits and collect every possible result.
Step 2: Split at first * (position 1)
Left is a single number [2]. Right "3-4*5" recurses further, producing [-17, -5, -1, 15]. Multiply each pair: 2*(-17)=-34, 2*(-5)=-10, 2*(-1)=-2, 2*15=30.
Step 3: Split at - (position 3)
Left "2*3" has one operator, giving [6]. Right "4*5" also gives [20]. Subtract: 6-20 = -14.
Step 4: Split at second * (position 5)
Left "2*3-4" has two operators, producing [2, -2]. Right is [5]. Multiply: 2*5=10, (-2)*5=-10.
Step 5: Collect all results
Merge the results from all three splits. There are 7 total values, corresponding to the 7 distinct ways to parenthesize this expression. For example, -34 comes from (2*(3-(4*(5)))), while -14 comes from ((2*3)-(4*5)).
Notice how the three top-level splits fan out into different recursive paths. The left sub-expression "3-4*5" itself has two operators, so it produces four results. The right sub-expression "2*3-4" produces two results. Each sub-problem follows the exact same splitting logic all the way down to single numbers.
Complexity analysis
| Approach | Time | Space |
|---|---|---|
| Divide and conquer | O(C(n)) Catalan | O(C(n)) |
Here n is the number of operators in the expression. The number of distinct parenthesizations of n operators follows the Catalan number sequence, which grows exponentially. Each parenthesization produces a result, and we enumerate all of them. The space complexity accounts for storing all results and the recursion stack.
You can add memoization by caching results for each substring. Since the same sub-expression can appear in multiple recursive paths, a memo dictionary keyed by the substring avoids redundant work. This does not change the worst-case output size, but it prevents recomputing the same sub-expression multiple times.
The building blocks
This problem is a clean example of the divide and conquer on expressions pattern. The core idea is: for every operator, split the problem into two halves, solve each half recursively, and combine all possible results.
Operator-based splitting. Instead of splitting at an index in an array, you split at each operator in a string. The left and right halves are themselves valid expressions. This is different from typical array partitioning because the split points are determined by the operators, not by arbitrary indices.
Cartesian product of sub-results. When the left half produces m results and the right half produces n results, you generate all m * n combinations. This "every pair" combination pattern appears whenever two independent sub-problems contribute to the final answer.
Catalan number structure. The number of ways to fully parenthesize an expression with n operators is the nth Catalan number. This is the same counting structure behind Unique Binary Search Trees and Generate Parentheses. Once you recognize the Catalan connection, the exponential nature of the output becomes expected rather than surprising.
Edge cases to watch for
- Single number: the expression
"42"has no operators, so the only result is[42]. The base case handles this. - Two numbers, one operator:
"3-1"has one split point, producing[2]. - Negative intermediate results: sub-expressions like
"3-4"evaluate to-1. Your combination logic must handle negative numbers correctly. - Leading zeros in numbers: the problem guarantees valid input, but expressions like
"10+5"contain multi-digit numbers. Usingint(expression)in the base case handles this naturally. - All same operators:
"1+1+1+1"still follows the same recursive splitting. The number of results equals the Catalan number for the operator count.
From understanding to recall
Walking through this problem once makes the divide and conquer idea clear. But reproducing the solution from memory during an interview is a different challenge. The tricky part is remembering the structure: scan for operators, split at each one, recurse on both halves, combine with a nested loop, and handle the base case when no operator is found.
Spaced repetition helps bridge that gap. By revisiting the operator-splitting pattern at increasing intervals, you build the muscle memory to write this recursive structure without hesitation. The divide and conquer on expressions pattern is one of many reusable building blocks that transfer across problems.
Related posts
- Generate Parentheses - Another problem rooted in Catalan number structure, generating all valid parenthesizations through backtracking
- Unique Binary Search Trees - Counting structures using the same root-splitting recurrence that drives this problem
- Combination Sum - Recursive enumeration of all valid combinations, showing the same "explore every branch" strategy