Facebook Pixel

282. Expression Add Operators

Problem Description

You are given a string num consisting only of digits and an integer target. Your task is to find all possible ways to insert the binary operators '+', '-', and '*' between the digits of num such that when the resulting expression is evaluated, it equals the target value.

Key requirements:

  • You can insert operators between any adjacent digits in the string
  • The three allowed operators are addition (+), subtraction (-), and multiplication (*)
  • Numbers in the resulting expressions cannot have leading zeros (e.g., "05" is not valid, but "0" alone is valid)
  • A number can span multiple consecutive digits (you don't have to put an operator between every digit)
  • Return all valid expressions that evaluate to the target

For example, if num = "123" and target = 6, possible valid expressions include:

  • "1+2+3" which evaluates to 6
  • "1*2*3" which evaluates to 6

The solution uses a depth-first search (DFS) approach with backtracking. It explores all possible ways to partition the string and insert operators, keeping track of:

  • The current position in the string (u)
  • The previous operand (prev) - needed for handling multiplication precedence
  • The current evaluation result (curr)
  • The expression path built so far

The algorithm handles multiplication precedence correctly by adjusting the current value when a multiplication operator is encountered, using the formula: curr - prev + prev * next. This effectively "undoes" the last operation and applies multiplication with the correct precedence.

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 involves a string of digits and mathematical operators, not nodes and edges as in graph problems.

Need to solve for kth smallest/largest?

  • No: We're not looking for the kth smallest or largest value, but rather all valid expressions that evaluate to a target.

Involves Linked Lists?

  • No: The problem deals with string manipulation and expression evaluation, not linked list operations.

Does the problem have small constraints?

  • Yes: The problem requires generating all possible combinations of operator placements between digits. The number of possibilities grows exponentially (for n digits, we have multiple choices at each position - whether to place an operator or continue the current number, and which operator to use). This exponential growth pattern and the need to explore all possibilities indicates small constraints are expected.

Brute force / Backtracking?

  • Yes: We need to:
    • Try all possible ways to partition the string into numbers
    • Try all three operators (+, -, *) at each partition point
    • Backtrack when a path doesn't lead to the target
    • Keep track of the current expression and evaluation state
    • Handle operator precedence (especially for multiplication)

Conclusion: The flowchart correctly leads us to use a Backtracking approach. This makes sense because we need to explore all possible combinations of number partitions and operator placements, building expressions incrementally and backtracking when necessary. The recursive DFS with backtracking allows us to efficiently explore the entire solution space while maintaining the state needed for correct expression evaluation.

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

Intuition

When we look at this problem, we need to explore all possible ways to place operators between digits. Think of it like placing dividers in the string - at each position between digits, we can either place an operator or continue building the current number.

The key insight is that this is a decision tree problem. At each step, we face multiple choices:

  1. How many digits should form the next number? (1 digit, 2 digits, 3 digits, etc.)
  2. Which operator should we use before this number? (+, -, or *)

For example, with "123", we could have:

  • "1+23", "12+3", "123" (different ways to partition)
  • For each partition, we have 3 operator choices

This screams backtracking - we need to try a choice, explore further, and if it doesn't work out, backtrack and try another choice.

The tricky part is handling multiplication because of operator precedence. When we encounter 2+3*4, we can't just evaluate left to right. We need to "undo" the last addition and apply multiplication first.

To handle this, we track two values:

  • curr: The running total of our expression
  • prev: The last operand we added/subtracted

When we multiply, we need to adjust: curr - prev + (prev * next). This removes the effect of the last operation and applies multiplication correctly.

For leading zeros, we need a simple check: if we're trying to form a multi-digit number starting with '0', we should stop - that's invalid.

The recursive nature of trying all possibilities, combined with the need to track state (current value, previous operand, position in string), naturally leads us to a DFS backtracking solution where we build the expression character by character and evaluate it simultaneously.

Solution Approach

The solution uses DFS with backtracking to explore all possible expressions. Let's walk through the implementation:

Core DFS Function Parameters:

  • u: Current index in the string where we're deciding what to do next
  • prev: The previous operand (needed for handling multiplication precedence)
  • curr: Current evaluation result of the expression so far
  • path: The expression string we're building

Main Algorithm Flow:

  1. Base Case: When u == len(num), we've processed the entire string. If curr == target, we found a valid expression and add it to our answer list.

  2. Recursive Exploration: From position u, we try forming numbers of different lengths:

    for i in range(u, len(num)):

    This loop considers taking 1 digit, 2 digits, 3 digits, etc. as the next number.

  3. Leading Zero Check:

    if i != u and num[u] == '0':
        break

    If we're trying to form a multi-digit number starting with '0', we stop. Single '0' is allowed.

  4. Extract Next Number:

    next = int(num[u : i + 1])

    Convert the substring to an integer.

  5. Handle First Number: When u == 0, there's no operator to place before the first number:

    dfs(i + 1, next, next, path + str(next))
  6. Try All Three Operators: For subsequent numbers, we recursively try each operator:

    • Addition:

      dfs(i + 1, next, curr + next, path + "+" + str(next))

      Simple - just add to current value.

    • Subtraction:

      dfs(i + 1, -next, curr - next, path + "-" + str(next))

      Store negative value as prev for potential future multiplication.

    • Multiplication:

      dfs(i + 1, prev * next, curr - prev + prev * next, path + "*" + str(next))

      The tricky part: curr - prev + prev * next

      • First, undo the last operation by subtracting prev
      • Then add prev * next to apply multiplication with correct precedence
      • Update prev to prev * next for potential future operations

Example Walkthrough with "123", target = 6:

  • Start with dfs(0, 0, 0, "")
  • Try "1" as first number: dfs(1, 1, 1, "1")
    • Try "1+2": dfs(2, 2, 3, "1+2")
      • Try "1+2+3": dfs(3, 3, 6, "1+2+3") → Success!
    • Try "1*2": dfs(2, 2, 2, "1*2")
      • Try "1*2*3": dfs(3, 6, 6, "1*2*3") → Success!

The backtracking happens automatically through the recursion - when we return from a recursive call, we're back to the previous state and can try the next option.

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 a small example with num = "105" and target = 5.

Starting State: dfs(0, 0, 0, "")

Step 1: Choose first number "1"

  • Extract "1" from position 0
  • Since it's the first number, no operator needed
  • Call: dfs(1, 1, 1, "1")

Step 2: From "1", try different options at position 1

Option 2a: Add "0"

  • Call: dfs(2, 0, 1, "1+0")
  • From here, try adding "5": dfs(3, 5, 6, "1+0+5") → 6 ≠ 5, backtrack
  • Try subtracting "5": dfs(3, -5, -4, "1+0-5") → -4 ≠ 5, backtrack
  • Try multiplying "5": dfs(3, 0, 1, "1*0*5") → 1 ≠ 5, backtrack

Option 2b: Subtract "0"

  • Call: dfs(2, 0, 1, "1-0")
  • From here, try adding "5": dfs(3, 5, 6, "1-0+5") → 6 ≠ 5, backtrack
  • Try subtracting "5": dfs(3, -5, -4, "1-0-5") → -4 ≠ 5, backtrack
  • Try multiplying "5": dfs(3, 0, 1, "1-0*5") → 1 ≠ 5, backtrack

Option 2c: Multiply "0"

  • Call: dfs(2, 0, 0, "1*0")
  • From here, try adding "5": dfs(3, 5, 5, "1*0+5")5 = 5, Found solution!

Option 2d: Try "05" as next number

  • Since "05" starts with '0' and has multiple digits, this is invalid - skip

Step 3: Back to start, try "10" as first number

  • Extract "10" from position 0
  • Call: dfs(2, 10, 10, "10")
  • From here, try subtracting "5": dfs(3, -5, 5, "10-5")5 = 5, Found solution!

Step 4: Try "105" as single number

  • Extract "105" from position 0
  • Call: dfs(3, 105, 105, "105") → 105 ≠ 5, backtrack

Final Result: ["1*0+5", "10-5"]

The key insight illustrated here is how multiplication precedence is handled. When we compute 1*0+5:

  1. First we have curr = 1, prev = 1
  2. We multiply by 0: curr = 1 - 1 + 1*0 = 0, prev = 0
  3. We add 5: curr = 0 + 5 = 5, prev = 5

This shows how the algorithm correctly evaluates 1*0 first before adding 5, respecting operator precedence.

Solution Implementation

1class Solution:
2    def addOperators(self, num: str, target: int) -> List[str]:
3        """
4        Find all possible ways to add operators (+, -, *) between digits of num to reach target.
5      
6        Args:
7            num: String containing only digits
8            target: Target value to reach
9          
10        Returns:
11            List of valid expressions that evaluate to target
12        """
13        result = []
14      
15        def backtrack(index: int, prev_operand: int, current_eval: int, expression: str) -> None:
16            """
17            Recursively build expressions by trying different operators and operands.
18          
19            Args:
20                index: Current position in the num string
21                prev_operand: Previous operand value (needed for multiplication precedence)
22                current_eval: Current evaluation result of the expression
23                expression: Current expression string being built
24            """
25            # Base case: reached end of string
26            if index == len(num):
27                if current_eval == target:
28                    result.append(expression)
29                return
30          
31            # Try all possible numbers starting from current index
32            for i in range(index, len(num)):
33                # Skip numbers with leading zeros (except single digit 0)
34                if i != index and num[index] == '0':
35                    break
36              
37                # Extract current number as operand
38                current_operand = int(num[index:i + 1])
39              
40                # First number in expression (no operator needed)
41                if index == 0:
42                    backtrack(i + 1, current_operand, current_operand, str(current_operand))
43                else:
44                    # Try addition
45                    backtrack(i + 1, 
46                             current_operand, 
47                             current_eval + current_operand, 
48                             expression + "+" + str(current_operand))
49                  
50                    # Try subtraction
51                    backtrack(i + 1, 
52                             -current_operand, 
53                             current_eval - current_operand, 
54                             expression + "-" + str(current_operand))
55                  
56                    # Try multiplication (need to handle precedence)
57                    # Undo the previous operation and apply multiplication first
58                    backtrack(i + 1, 
59                             prev_operand * current_operand, 
60                             current_eval - prev_operand + prev_operand * current_operand, 
61                             expression + "*" + str(current_operand))
62      
63        # Start the recursive exploration
64        backtrack(0, 0, 0, "")
65        return result
66
1class Solution {
2    // Store all valid expressions that evaluate to target
3    private List<String> result;
4    // Input number string
5    private String numString;
6    // Target value to achieve
7    private int targetValue;
8
9    /**
10     * Find all possible expressions by adding operators (+, -, *) between digits
11     * that evaluate to the target value
12     * @param num String containing digits
13     * @param target Target value for expressions
14     * @return List of valid expressions
15     */
16    public List<String> addOperators(String num, int target) {
17        result = new ArrayList<>();
18        this.numString = num;
19        this.targetValue = target;
20      
21        // Start DFS from index 0 with initial values
22        backtrack(0, 0, 0, "");
23        return result;
24    }
25
26    /**
27     * Recursive backtracking to generate all possible expressions
28     * @param index Current position in the number string
29     * @param previousOperand Value of the previous operand (for handling multiplication precedence)
30     * @param currentValue Current evaluation result of the expression
31     * @param expression Current expression being built
32     */
33    private void backtrack(int index, long previousOperand, long currentValue, String expression) {
34        // Base case: reached end of string
35        if (index == numString.length()) {
36            // Check if current expression evaluates to target
37            if (currentValue == targetValue) {
38                result.add(expression);
39            }
40            return;
41        }
42      
43        // Try different lengths of numbers starting from current index
44        for (int endIndex = index; endIndex < numString.length(); endIndex++) {
45            // Skip numbers with leading zeros (except single digit 0)
46            if (endIndex != index && numString.charAt(index) == '0') {
47                break;
48            }
49          
50            // Extract current number from substring
51            long currentNumber = Long.parseLong(numString.substring(index, endIndex + 1));
52          
53            // First number in expression (no operator before it)
54            if (index == 0) {
55                backtrack(endIndex + 1, currentNumber, currentNumber, expression + currentNumber);
56            } else {
57                // Try addition operator
58                backtrack(endIndex + 1, currentNumber, currentValue + currentNumber, 
59                         expression + "+" + currentNumber);
60              
61                // Try subtraction operator
62                backtrack(endIndex + 1, -currentNumber, currentValue - currentNumber, 
63                         expression + "-" + currentNumber);
64              
65                // Try multiplication operator (need to handle precedence)
66                // Undo the previous operation and apply multiplication first
67                backtrack(endIndex + 1, previousOperand * currentNumber, 
68                         currentValue - previousOperand + previousOperand * currentNumber, 
69                         expression + "*" + currentNumber);
70            }
71        }
72    }
73}
74
1class Solution {
2private:
3    // Store all valid expressions that evaluate to target
4    vector<string> result;
5    // Input number string
6    string numString;
7    // Target value to achieve
8    int targetValue;
9  
10    /**
11     * Recursive backtracking to generate all possible expressions
12     * @param index Current position in the number string
13     * @param previousOperand Value of the previous operand (for handling multiplication precedence)
14     * @param currentValue Current evaluation result of the expression
15     * @param expression Current expression being built
16     */
17    void backtrack(int index, long previousOperand, long currentValue, string expression) {
18        // Base case: reached end of string
19        if (index == numString.length()) {
20            // Check if current expression evaluates to target
21            if (currentValue == targetValue) {
22                result.push_back(expression);
23            }
24            return;
25        }
26      
27        // Try different lengths of numbers starting from current index
28        for (int endIndex = index; endIndex < numString.length(); endIndex++) {
29            // Skip numbers with leading zeros (except single digit 0)
30            if (endIndex != index && numString[index] == '0') {
31                break;
32            }
33          
34            // Extract current number from substring
35            string currentNumStr = numString.substr(index, endIndex - index + 1);
36            long currentNumber = stol(currentNumStr);
37          
38            // First number in expression (no operator before it)
39            if (index == 0) {
40                backtrack(endIndex + 1, currentNumber, currentNumber, 
41                         expression + currentNumStr);
42            } else {
43                // Try addition operator
44                backtrack(endIndex + 1, currentNumber, currentValue + currentNumber, 
45                         expression + "+" + currentNumStr);
46              
47                // Try subtraction operator
48                backtrack(endIndex + 1, -currentNumber, currentValue - currentNumber, 
49                         expression + "-" + currentNumStr);
50              
51                // Try multiplication operator (need to handle precedence)
52                // Undo the previous operation and apply multiplication first
53                backtrack(endIndex + 1, previousOperand * currentNumber, 
54                         currentValue - previousOperand + previousOperand * currentNumber, 
55                         expression + "*" + currentNumStr);
56            }
57        }
58    }
59  
60public:
61    /**
62     * Find all possible expressions by adding operators (+, -, *) between digits
63     * that evaluate to the target value
64     * @param num String containing digits
65     * @param target Target value for expressions
66     * @return List of valid expressions
67     */
68    vector<string> addOperators(string num, int target) {
69        result.clear();
70        this->numString = num;
71        this->targetValue = target;
72      
73        // Start DFS from index 0 with initial values
74        backtrack(0, 0, 0, "");
75        return result;
76    }
77};
78
1// Store all valid expressions that evaluate to target
2let result: string[];
3// Input number string
4let numString: string;
5// Target value to achieve
6let targetValue: number;
7
8/**
9 * Find all possible expressions by adding operators (+, -, *) between digits
10 * that evaluate to the target value
11 * @param num String containing digits
12 * @param target Target value for expressions
13 * @return List of valid expressions
14 */
15function addOperators(num: string, target: number): string[] {
16    result = [];
17    numString = num;
18    targetValue = target;
19  
20    // Start DFS from index 0 with initial values
21    backtrack(0, 0, 0, "");
22    return result;
23}
24
25/**
26 * Recursive backtracking to generate all possible expressions
27 * @param index Current position in the number string
28 * @param previousOperand Value of the previous operand (for handling multiplication precedence)
29 * @param currentValue Current evaluation result of the expression
30 * @param expression Current expression being built
31 */
32function backtrack(index: number, previousOperand: number, currentValue: number, expression: string): void {
33    // Base case: reached end of string
34    if (index === numString.length) {
35        // Check if current expression evaluates to target
36        if (currentValue === targetValue) {
37            result.push(expression);
38        }
39        return;
40    }
41  
42    // Try different lengths of numbers starting from current index
43    for (let endIndex = index; endIndex < numString.length; endIndex++) {
44        // Skip numbers with leading zeros (except single digit 0)
45        if (endIndex !== index && numString.charAt(index) === '0') {
46            break;
47        }
48      
49        // Extract current number from substring
50        const currentNumber = Number(numString.substring(index, endIndex + 1));
51      
52        // First number in expression (no operator before it)
53        if (index === 0) {
54            backtrack(endIndex + 1, currentNumber, currentNumber, expression + currentNumber);
55        } else {
56            // Try addition operator
57            backtrack(endIndex + 1, currentNumber, currentValue + currentNumber, 
58                     expression + "+" + currentNumber);
59          
60            // Try subtraction operator
61            backtrack(endIndex + 1, -currentNumber, currentValue - currentNumber, 
62                     expression + "-" + currentNumber);
63          
64            // Try multiplication operator (need to handle precedence)
65            // Undo the previous operation and apply multiplication first
66            backtrack(endIndex + 1, previousOperand * currentNumber, 
67                     currentValue - previousOperand + previousOperand * currentNumber, 
68                     expression + "*" + currentNumber);
69        }
70    }
71}
72

Time and Space Complexity

Time Complexity: O(n * 3^n)

The time complexity analysis breaks down as follows:

  • At each position in the string, we can choose to extend the current number or place an operator before starting a new number
  • For a string of length n, we have at most n-1 positions where we can place operators
  • At each position where we place an operator, we have 3 choices: +, -, or *
  • In the worst case, we explore all possible combinations of operators between digits
  • For each valid expression generated, we perform operations that take O(n) time (string concatenation and integer parsing)
  • The total number of expressions we can generate is bounded by O(3^n) since we make up to 3 recursive calls for each position
  • Therefore, the overall time complexity is O(n * 3^n)

Space Complexity: O(n)

The space complexity analysis:

  • The recursion depth is at most O(n) where n is the length of the input string
  • Each recursive call uses O(1) space for its local variables (u, prev, curr, next)
  • The path parameter accumulates the expression string, which can be at most O(n) in length (considering operators and digits)
  • The ans list stores all valid expressions, but this is considered as output space and typically not counted in space complexity analysis
  • The call stack space dominates, giving us O(n) space complexity

Common Pitfalls

1. Incorrect Multiplication Precedence Handling

The Pitfall: The most common mistake is not properly handling multiplication precedence. Many initial attempts incorrectly evaluate expressions left-to-right without considering that multiplication should be performed before addition/subtraction.

Incorrect Approach:

# WRONG: Simply applying operators left to right
if operator == '*':
    current_eval = current_eval * next_number  # This is wrong!

For example, with expression "2+3*4":

  • Wrong evaluation: (2+3)*4 = 20
  • Correct evaluation: 2+(3*4) = 14

The Solution: Track the previous operand and use the formula curr - prev + prev * next:

# CORRECT: Undo last operation, then apply multiplication
backtrack(i + 1, 
         prev_operand * current_operand,  # New previous operand
         current_eval - prev_operand + prev_operand * current_operand,  # Adjusted evaluation
         expression + "*" + str(current_operand))

2. Leading Zeros Not Properly Handled

The Pitfall: Forgetting to check for leading zeros can generate invalid expressions like "05+2" or "00+3".

Incorrect Approach:

# WRONG: No check for leading zeros
for i in range(index, len(num)):
    current_operand = int(num[index:i + 1])
    # Process without checking...

The Solution: Add a check that breaks the loop when encountering a multi-digit number starting with '0':

# CORRECT: Check for leading zeros
for i in range(index, len(num)):
    if i != index and num[index] == '0':
        break  # Stop if trying to form multi-digit number with leading zero
    current_operand = int(num[index:i + 1])

3. Incorrect Previous Operand for Subtraction

The Pitfall: When performing subtraction, forgetting to store the negative value as the previous operand leads to incorrect multiplication handling later.

Incorrect Approach:

# WRONG: Storing positive value for subtraction
backtrack(i + 1, current_operand, current_eval - current_operand, expression + "-" + str(current_operand))

This causes problems with expressions like "6-2*3":

  • Wrong: 6-23 = 6-2+23 = 10 (if prev is stored as positive 2)
  • Correct: 6-2*3 = 6-(-2)+(-2)*3 = 0

The Solution: Store the negative value as the previous operand:

# CORRECT: Store negative value for subtraction
backtrack(i + 1, 
         -current_operand,  # Negative value as previous operand
         current_eval - current_operand, 
         expression + "-" + str(current_operand))

4. Integer Overflow with Large Numbers

The Pitfall: The problem constraints might include very long digit strings, leading to numbers that exceed integer limits.

Example Problem:

num = "999999999999"  # Could cause overflow when converted to int

The Solution: Check the problem constraints and handle accordingly:

# Add overflow check if needed
current_operand_str = num[index:i + 1]
if len(current_operand_str) > 10:  # Assuming 32-bit integers
    continue  # Skip this combination
current_operand = int(current_operand_str)

5. Not Handling Edge Cases Properly

The Pitfall: Missing edge cases like single digit strings or when the target is 0.

Common Edge Cases to Consider:

  • num = "0", target = 0 → Should return ["0"]
  • num = "00", target = 0 → Should return ["0+0", "0-0", "0*0"]
  • num = "105", target = 5 → Should handle the '0' in the middle correctly

The Solution: The current implementation handles these correctly, but always test with:

# Test edge cases
assert "0" in solution.addOperators("0", 0)
assert len(solution.addOperators("00", 0)) == 3
assert "10-5" in solution.addOperators("105", 5)
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Consider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?


Recommended Readings

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

Load More