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.
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:
- How many digits should form the next number? (1 digit, 2 digits, 3 digits, etc.)
- 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 expressionprev
: 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 nextprev
: The previous operand (needed for handling multiplication precedence)curr
: Current evaluation result of the expression so farpath
: The expression string we're building
Main Algorithm Flow:
-
Base Case: When
u == len(num)
, we've processed the entire string. Ifcurr == target
, we found a valid expression and add it to our answer list. -
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.
-
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.
-
Extract Next Number:
next = int(num[u : i + 1])
Convert the substring to an integer.
-
Handle First Number: When
u == 0
, there's no operator to place before the first number:dfs(i + 1, next, next, path + str(next))
-
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
toprev * next
for potential future operations
- First, undo the last operation by subtracting
-
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
- Try
"1*2"
:dfs(2, 2, 2, "1*2")
- Try
"1*2*3"
:dfs(3, 6, 6, "1*2*3")
→ Success!
- Try
- Try
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 EvaluatorExample 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
:
- First we have
curr = 1, prev = 1
- We multiply by 0:
curr = 1 - 1 + 1*0 = 0, prev = 0
- 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 mostn-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)
wheren
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 mostO(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)
Consider the classic dynamic programming of fibonacci numbers, what is the recurrence relation?
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!