A backtracking solution for inserting operators into a numeric string so the expression evaluates to a target value.
Problem Restatement
We are given:
- A string
numcontaining only digits. - An integer
target.
We need to insert the operators:
+, -, *between the digits so that the final mathematical expression evaluates exactly to target.
We must return all valid expressions.
Digits must remain in the original order.
We may also choose to join consecutive digits into a larger number.
For example:
"123"can become:
"1+2+3"
"12-3"
"1*23"but not:
"3+2+1"because digit order cannot change.
Numbers with leading zeros are invalid except for the single digit "0".
So:
"0"is valid, but:
"05"
"00"are invalid multi-digit numbers.
The official statement asks us to return all expressions formed by inserting binary operators between digits so that the expression evaluates to target. The input size is small enough for exhaustive search with pruning. (leetcode.com)
Input and Output
| Item | Meaning |
|---|---|
| Input | String num, integer target |
| Output | List of valid expressions |
| Operators | +, -, * |
| Rule | Digit order cannot change |
| Rule | Multi-digit numbers cannot start with 0 |
Function shape:
class Solution:
def addOperators(self, num: str, target: int) -> list[str]:
...Examples
For:
num = "123"
target = 6Valid expressions are:
[
"1+2+3",
"1*2*3"
]because:
1 + 2 + 3 = 6
1 * 2 * 3 = 6For:
num = "232"
target = 8Valid expressions are:
[
"2*3+2",
"2+3*2"
]For:
num = "105"
target = 5Valid expressions are:
[
"1*0+5",
"10-5"
]Notice:
"1*05"is invalid because "05" has a leading zero.
First Thought: Generate Everything
We could try every possible way to insert operators.
Between every pair of digits, we may choose:
+
-
*
nothingFor example:
"123"can produce:
1+2+3
1-2+3
12+3
1*23
...After generating each expression, we could evaluate it.
This works conceptually, but evaluating expressions correctly becomes difficult because multiplication has higher precedence.
For example:
1 + 2 * 3must evaluate to:
7not:
9We need a cleaner way to track the value during construction.
Key Insight
We build the expression left to right using DFS.
At every step, we choose:
- The next number segment.
- Which operator to place before it.
The difficult part is multiplication.
For addition and subtraction, evaluation is easy:
current + value
current - valueBut multiplication changes the previous term because multiplication has higher precedence.
Example:
1 + 2 * 3When we already have:
1 + 2 = 3and then see:
*3we cannot simply do:
3 * 3Instead:
1 + (2 * 3)So we must remove the last added term and replace it with the multiplied value.
We store:
| Variable | Meaning |
|---|---|
value | Current evaluated result |
prev | Last signed number added into the expression |
For multiplication:
new_value = value - prev + (prev * current)This correctly handles precedence.
Algorithm
Use DFS with these parameters:
| Parameter | Meaning |
|---|---|
index | Current position in num |
path | Current expression string |
value | Current evaluated value |
prev | Last signed operand |
At each step:
- Choose the next number substring.
- Skip invalid leading-zero numbers.
- If this is the first number, start the expression.
- Otherwise try:
+-*
For multiplication:
value - prev + prev * currentWhen we reach the end of the string:
index == len(num)check whether:
value == targetIf yes, store the expression.
Correctness
The DFS explores every valid way to partition the digit string into numbers and insert operators between them.
At every recursive call, the expression represented by path evaluates exactly to value.
For addition:
value + currentcorrectly appends a positive term.
For subtraction:
value - currentcorrectly appends a negative term.
For multiplication, the previous operand prev has already been included in value.
Suppose the current expression evaluates as:
value = earlier + prevWhen we append:
* currentthe multiplication must bind only to prev.
So the new value becomes:
earlier + (prev * current)which equals:
value - prev + (prev * current)Therefore multiplication precedence is handled correctly.
The DFS tries all valid partitions and all valid operator choices, except numbers with illegal leading zeros. So every valid expression is generated exactly once.
When the recursion reaches the end of the digit string, the algorithm adds the expression only if its evaluated value equals target. Therefore every returned expression is correct.
Complexity
Let n = len(num).
| Metric | Value | Why |
|---|---|---|
| Time | Exponential | We explore many partitions and operator combinations |
| Space | O(n) recursion depth | Maximum recursion depth equals string length |
The branching factor is high because each gap may contain multiple substring choices and multiple operators.
This is expected for exhaustive backtracking problems.
Implementation
class Solution:
def addOperators(self, num: str, target: int) -> list[str]:
result = []
def dfs(index, path, value, prev):
if index == len(num):
if value == target:
result.append(path)
return
for end in range(index, len(num)):
if end > index and num[index] == "0":
break
current_str = num[index:end + 1]
current = int(current_str)
if index == 0:
dfs(
end + 1,
current_str,
current,
current
)
else:
dfs(
end + 1,
path + "+" + current_str,
value + current,
current
)
dfs(
end + 1,
path + "-" + current_str,
value - current,
-current
)
dfs(
end + 1,
path + "*" + current_str,
value - prev + prev * current,
prev * current
)
dfs(0, "", 0, 0)
return resultCode Explanation
We store valid expressions here:
result = []The DFS state contains:
(index, path, value, prev)index tells where we are in the digit string.
path stores the current expression.
value stores the evaluated result.
prev stores the most recent signed operand.
When we reach the end:
if index == len(num):we check whether the expression equals the target:
if value == target:
result.append(path)The loop chooses the next number substring:
for end in range(index, len(num)):This allows numbers like:
1
12
123We reject leading-zero numbers:
if end > index and num[index] == "0":
breakThe first number is special because it does not need an operator before it:
if index == 0:After the first number, we try all three operators.
Addition:
value + currentSubtraction:
value - currentMultiplication:
value - prev + prev * currentThis rewrites the last operand to respect multiplication precedence.
Testing
def test_add_operators():
s = Solution()
assert sorted(s.addOperators("123", 6)) == sorted([
"1+2+3",
"1*2*3"
])
assert sorted(s.addOperators("232", 8)) == sorted([
"2*3+2",
"2+3*2"
])
assert sorted(s.addOperators("105", 5)) == sorted([
"1*0+5",
"10-5"
])
assert sorted(s.addOperators("00", 0)) == sorted([
"0+0",
"0-0",
"0*0"
])
assert s.addOperators("3456237490", 9191) == []
print("all tests passed")
test_add_operators()Test meaning:
| Test | Why |
|---|---|
"123", 6 | Basic addition and multiplication |
"232", 8 | Multiplication precedence |
"105", 5 | Leading-zero handling |
"00", 0 | Multiple valid zero expressions |
"3456237490", 9191 | No valid expression exists |