LeetCode 964: Least Operators to Express Number
A clear explanation of expressing a target using the fewest operators with repeated uses of x.
Problem Restatement
We are given two integers:
x
target
We may build expressions using only the number x and the operators:
+
-
*
/
Division is real division, and standard operator precedence applies.
We cannot use unary negation. So:
-x
is not allowed directly.
We want to express target using the minimum number of operators.
Return that minimum number.
The official constraints are:
| Constraint | Value |
|---|---|
2 <= x <= 100 |
Base number |
1 <= target <= 2 * 10^8 |
Desired value |
Source: LeetCode problem statement.
Input and Output
| Item | Meaning |
|---|---|
| Input | Integer x |
| Input | Integer target |
| Output | Minimum number of operators needed |
| Allowed numbers | Only repeated uses of x |
Example function shape:
def leastOpsExpressTarget(x: int, target: int) -> int:
...
Examples
Example 1:
x = 3
target = 19
Output:
5
One optimal expression is:
3 * 3 + 3 * 3 + 3 / 3
This equals:
9 + 9 + 1 = 19
Operator count:
*
+
*
+
/
Total:
5
Example 2:
x = 5
target = 501
Output:
8
Example 3:
x = 100
target = 100000000
Output:
3
Expression:
100 * 100 * 100 * 100
This uses three multiplication operators.
First Thought: Build Expressions Directly
We could try generating expressions recursively.
At each step:
- multiply by
x - divide by
x - add another term
- subtract another term
But the number of expressions grows explosively.
We need a mathematical structure.
Key Insight
The expression behaves like writing the target in base x.
Example:
x = 3
target = 19
Base 3 representation:
19 = 2 * 9 + 0 * 3 + 1
That means:
19 = 2 * (3^2) + 1
The powers of x are important:
x^0 = 1
x^1 = x
x^2 = x * x
x^3 = x * x * x
Each power has a cost.
To build:
x^k
we need:
k
multiplication operators.
But there is one special case:
1 = x / x
which costs one division operator.
So:
| Value | Cost |
|---|---|
x^0 = 1 |
2 operands but 1 operator |
x^k |
k operators |
Recursive Choice
Suppose:
target = q * x + r
where:
0 <= r < x
We have two choices.
Choice 1: Build Upward
Use:
r copies
of the lower power.
Example:
target = 19
x = 3
19 = 6 * 3 + 1
We recursively build:
6
then add one more unit.
Choice 2: Overshoot Then Subtract
Instead of using r, use:
x - r
and subtract from the next higher multiple.
Example:
19 = 7 * 3 - 2
Sometimes subtracting fewer pieces is cheaper than adding many pieces.
So we take the minimum of the two strategies.
Dynamic Programming Interpretation
Define:
dfs(t)
as the minimum operator cost to build t.
We recursively divide by x:
q, r = divmod(t, x)
Then compare:
build q and add r copies
versus:
build q + 1 and subtract x - r copies
Memoization avoids recomputing states.
Base Cases
If:
t == 0
then no operators are needed.
If:
t < x
then there are two ways to build t.
Add Small Units
Build:
t * (x / x)
Cost:
2 * t - 1
because each 1 costs division, and additions connect them.
Subtract From x
Build:
x - (x - t)
Cost:
2 * (x - t)
We choose the cheaper one.
Correctness
We prove that the recursive relation computes the minimum number of operators.
Every valid expression ultimately builds the target using powers of x. Dividing the target by x gives:
target = q * x + r
where 0 <= r < x.
Any expression for target must resolve this remainder in one of two ways.
The first possibility is to construct q * x and then add r.
The second possibility is to construct (q + 1) * x and subtract (x - r).
These are the only two nearest multiples of x, so every optimal expression must follow one of these two strategies.
The recursive calls compute the optimal cost for the quotient part. The remainder adjustment cost is added according to how many low-power terms are needed.
For targets smaller than x, the algorithm directly compares the only two meaningful constructions:
1 + 1 + ...
versus:
x - ...
and chooses the smaller operator count.
Memoization guarantees that each subproblem is solved optimally once and reused consistently.
Therefore, the recursion explores all optimal constructions and returns the minimum operator count.
Complexity
The recursion repeatedly divides the target by x.
| Metric | Value | Why |
|---|---|---|
| Time | O(log_x(target)) |
Each recursive step reduces the target size |
| Space | O(log_x(target)) |
Memoization and recursion depth |
Implementation
from functools import cache
class Solution:
def leastOpsExpressTarget(self, x: int, target: int) -> int:
@cache
def dfs(t: int) -> int:
if t == 0:
return 0
if t < x:
return min(
2 * t - 1,
2 * (x - t),
)
q, r = divmod(t, x)
use_positive = dfs(q) + r
use_negative = dfs(q + 1) + (x - r)
if q > 0:
use_positive += 1
if r > 0:
use_negative += 1
return min(use_positive, use_negative)
return dfs(target)
Code Explanation
We memoize recursive results:
@cache
so repeated subproblems are computed only once.
If:
t == 0
then no expression is needed:
return 0
For small targets:
if t < x:
we compare two constructions.
Build directly with ones:
2 * t - 1
Example:
2 = x/x + x/x
This uses:
/
+
which is 3 operators.
Or subtract from x:
x - (x/x + ...)
with cost:
2 * (x - t)
For larger targets:
q, r = divmod(t, x)
We recursively solve the quotient.
Positive construction:
use_positive = dfs(q) + r
Negative construction:
use_negative = dfs(q + 1) + (x - r)
Then we add operator costs for connecting pieces:
if q > 0:
use_positive += 1
and:
if r > 0:
use_negative += 1
Finally:
return min(use_positive, use_negative)
chooses the cheaper construction.
Testing
def run_tests():
s = Solution()
assert s.leastOpsExpressTarget(3, 19) == 5
assert s.leastOpsExpressTarget(5, 501) == 8
assert s.leastOpsExpressTarget(100, 100000000) == 3
assert s.leastOpsExpressTarget(3, 1) == 1
assert s.leastOpsExpressTarget(3, 2) == 2
assert s.leastOpsExpressTarget(2, 7) == 4
assert s.leastOpsExpressTarget(2, 1) == 1
assert s.leastOpsExpressTarget(10, 10) == 0
print("all tests passed")
run_tests()
Test meaning:
| Test | Why |
|---|---|
(3, 19) |
Official-style mixed construction |
(5, 501) |
Larger recursive decomposition |
(100, 100000000) |
Pure multiplication chain |
(3, 1) |
Smallest nonzero target |
(3, 2) |
Small target below x |
(2, 7) |
Binary-style recursive behavior |
(2, 1) |
Single division construction |
(10, 10) |
Exact match with no operators needed |