964. Least Operators to Express Number
Problem Description
You are given a positive integer x
and need to create a mathematical expression using only the number x
and the operators +
, -
, *
, /
to equal a target value. The expression must follow the format: x (op1) x (op2) x (op3) x ...
where each operator is one of the four basic operations.
For example, if x = 3
and you want to reach a certain target, you could write expressions like:
3 * 3 / 3 + 3 - 3
which equals3
3 + 3 + 3
which equals9
3 * 3 - 3 / 3
which equals8
The expression must follow these rules:
- Division returns rational numbers (not just integers)
- No parentheses are allowed anywhere in the expression
- Standard order of operations applies (multiplication and division before addition and subtraction)
- Unary negation is not allowed (you cannot write
-x
, only operations likex - x
are valid)
Your task is to find the minimum number of operators needed to create an expression that equals the given target
.
For instance, if x = 3
and target = 19
, one possible expression is 3 + 3 * 3 + 3 + 3 / 3
, which uses 5 operators and equals 19. The function should return the minimum number of operators required.
The solution uses dynamic programming with memoization to explore different ways of building up to the target value. It considers different powers of x
and decides whether to add or subtract values to reach the target efficiently. The key insight is that any target can be expressed as a combination of powers of x
(including x^0 = 1
represented by x/x
, and x^(-1)
represented by division), and the algorithm finds the most efficient combination.
Intuition
The key insight is to think about what values we can create using x
and how many operators each requires. Let's break this down:
x
itself requires 0 operatorsx + x = 2x
requires 1 operatorx - x = 0
requires 1 operatorx * x = x²
requires 1 operatorx / x = 1
requires 1 operator
Notice that we can build powers of x
:
x² = x * x
(1 operator)xÂł = x * x * x
(2 operators)x^k
requiresk-1
operators
We can also create 1
using x / x
(1 operator), and from there build any small integer by adding 1
s together. For example, to get 3
when x = 5
, we could do 5 / 5 + 5 / 5 + 5 / 5
.
The crucial realization is that any target number can be represented as a combination of powers of x
. Think of it like converting the target to base x
, but with the twist that we can use both positive and negative coefficients.
For any value v
we want to reach:
-
If
v
is small (less than or equal tox
), we can either:- Build it using
1
s:x/x + x/x + ...
(requires2v - 1
operators total) - Or subtract from
x
:x - (something)
where we recursively find how to buildx - v
- Build it using
-
If
v
is large, we find the appropriate power ofx
:- Find
k
such thatx^k
is close tov
- Either overshoot with
x^k
and subtract the difference:x^k - (x^k - v)
- Or undershoot with
x^(k-1)
and add the difference:x^(k-1) + (v - x^(k-1))
- Find
The dynamic programming approach with memoization naturally emerges because we're breaking down the problem of reaching target
into subproblems of reaching smaller values. Each recursive call represents finding the optimal way to express a particular value, and we cache these results to avoid redundant calculations.
The algorithm essentially performs a search through different decompositions of the target, always choosing the path that minimizes the total number of operators used.
Learn more about Memoization, Math and Dynamic Programming patterns.
Solution Approach
The solution uses a recursive approach with memoization to find the minimum number of operators needed to express the target value.
The main function dfs(v)
computes the minimum operators needed to create value v
. Here's how it works:
Base Case: When x >= v
When the value we want to create (v
) is less than or equal to x
, we have two strategies:
-
Build using ones: Create
v
by adding1
s together. Since each1
is created usingx/x
(1 operator), and we needv
of them withv-1
addition operators between them, the total is2v - 1
operators.- Example: If
x = 5
andv = 3
, we get5/5 + 5/5 + 5/5
using 5 operators total.
- Example: If
-
Subtract from x: Express
v
asx - (x - v)
. This uses 1 subtraction operator plus the operators needed to createx - v
. The total is2 * (x - v)
operators (since creatingx - v
using ones would need2(x - v) - 1
operators, plus 1 for the subtraction).
The algorithm chooses the minimum of these two options: min(v * 2 - 1, 2 * (x - v))
.
Recursive Case: When x < v
For larger values, we need to use powers of x
:
-
Find the appropriate power: The algorithm finds the smallest
k
such thatx^k >= v
. This is done through a simple loop:k = 2 while x**k < v: k += 1
-
Decision point: Once we have
x^k
, we can either:- Overshoot and subtract: Use
x^k - (x^k - v)
to getv
. This requiresk
operators to buildx^k
(sincex^k
needsk-1
multiplications), plus the operators to buildx^k - v
. - Undershoot and add: Use
x^(k-1) + (v - x^(k-1))
to getv
. This requiresk-1
operators forx^(k-1)
, plus the operators to buildv - x^(k-1)
.
- Overshoot and subtract: Use
-
Optimization: The code includes a smart optimization. If
x^k - v < v
, meaning the overshoot difference is smaller thanv
itself, it considers both options. Otherwise, it only uses the undershoot approach since the overshoot would require building a larger value recursively.
Memoization with @cache
The @cache
decorator stores previously computed results for each value of v
. This prevents redundant calculations when the same subproblem appears multiple times during recursion, significantly improving performance.
Final Result
The function returns dfs(target)
, which gives the minimum number of operators needed to express the target value using only the number x
.
The elegance of this solution lies in its recursive decomposition - it transforms the problem of expressing any number into smaller subproblems, leveraging the fact that any target can be efficiently expressed as a combination of powers of x
with appropriate additions and subtractions.
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 finding the minimum operators needed when x = 3
and target = 10
.
Initial Call: dfs(10)
Since x = 3 < 10
, we enter the recursive case and need to find an appropriate power of 3:
3^2 = 9
(less than 10)3^3 = 27
(greater than 10)
So k = 3
, meaning 3^3 = 27
is our overshoot power.
Exploring Two Options:
- Overshoot approach: Express 10 as
27 - 17
- We need
k - 1 = 2
operators to build3^3
(i.e.,3 * 3 * 3
) - We need 1 subtraction operator
- We need
dfs(17)
operators to build 17
- We need
- Undershoot approach: Express 10 as
9 + 1
- We need
k - 2 = 1
operator to build3^2
(i.e.,3 * 3
) - We need 1 addition operator
- We need
dfs(1)
operators to build 1
- We need
Computing dfs(1)
:
Since x = 3 >= 1
, we use the base case:
- Option 1: Build 1 using ones:
3/3
requires 1 operator - Option 2: Subtract from 3:
3 - 2
requires2 * (3 - 1) = 4
operators
Minimum is 1 operator, so dfs(1) = 1
.
Computing dfs(17)
:
Since 3 < 17
, we find powers:
3^2 = 9
(less than 17)3^3 = 27
(greater than 17)
So k = 3
.
Checking overshoot: 27 - 17 = 10
. Since 10 < 17
, we consider both options:
- Overshoot:
27 - 10
needs2 + dfs(10)
operators - Undershoot:
9 + 8
needs1 + dfs(8)
operators
For dfs(8)
:
- Since
3 < 8
, we find3^2 = 9 > 8
, sok = 2
- Overshoot:
9 - 1
needs1 + dfs(1) = 1 + 1 = 2
operators - Since we only need the minimum and
9 - 8 = 1 < 8
, this gives usdfs(8) = 2
Back to dfs(17)
:
- Overshoot would create a cycle (needs
dfs(10)
which we're computing) - Undershoot:
1 + dfs(8) = 1 + 2 = 3
operators
So dfs(17) = 3
.
Final Calculation for dfs(10)
:
- Overshoot:
2 + dfs(17) = 2 + 3 = 5
operators - Undershoot:
1 + dfs(1) = 1 + 1 = 2
operators
The minimum is 2 operators.
Verifying the Result:
The expression 3 * 3 + 3 / 3
equals 9 + 1 = 10
and uses exactly 2 operators (one multiplication and one division, with addition connecting them). This confirms our answer of 2 operators.
Solution Implementation
1class Solution:
2 def leastOpsExpressTarget(self, x: int, target: int) -> int:
3 """
4 Find the minimum number of operations needed to build target using x.
5 Operations allowed: +, -, *, / (where / means x/x = 1)
6
7 Args:
8 x: The base number to use in expressions
9 target: The target value to build
10
11 Returns:
12 Minimum number of operators needed (excluding the first term)
13 """
14 from functools import cache
15
16 @cache
17 def dfs(current_value: int) -> int:
18 """
19 Recursively find minimum operations to build current_value.
20
21 Args:
22 current_value: The value we need to construct
23
24 Returns:
25 Minimum number of operations needed
26 """
27 # Base case: when x is greater than or equal to current_value
28 if x >= current_value:
29 # Option 1: Use addition with x/x (1) repeated current_value times
30 # Each 1 needs one division operator, plus (current_value - 1) additions
31 option_add_ones = current_value * 2 - 1
32
33 # Option 2: Use x minus some number of 1s
34 # One subtraction for x, then (x - current_value) divisions for 1s,
35 # plus (x - current_value - 1) subtractions between 1s
36 option_subtract_from_x = 2 * (x - current_value)
37
38 return min(option_add_ones, option_subtract_from_x)
39
40 # Find the smallest power k where x^k >= current_value
41 power = 2
42 while x ** power < current_value:
43 power += 1
44
45 # Check if it's better to overshoot and subtract or undershoot and add
46 if x ** power - current_value < current_value:
47 # If overshooting is closer, we have two options:
48 # Option 1: Use x^power and subtract the difference
49 option_overshoot = power + dfs(x ** power - current_value)
50
51 # Option 2: Use x^(power-1) and add the difference
52 option_undershoot = power - 1 + dfs(current_value - x ** (power - 1))
53
54 return min(option_overshoot, option_undershoot)
55 else:
56 # If undershooting is closer or equal, use x^(power-1) and add
57 return power - 1 + dfs(current_value - x ** (power - 1))
58
59 return dfs(target)
60
1class Solution {
2 private int base; // The base number x for building expressions
3 private Map<Integer, Integer> memo = new HashMap<>(); // Memoization cache for dynamic programming
4
5 /**
6 * Find the minimum number of operations to build target using x with +, -, *, /
7 * @param x The base number to use in expressions
8 * @param target The target value to achieve
9 * @return Minimum number of operations needed
10 */
11 public int leastOpsExpressTarget(int x, int target) {
12 this.base = x;
13 return findMinOperations(target);
14 }
15
16 /**
17 * Recursive helper to find minimum operations for a given value
18 * @param value The current value we need to build
19 * @return Minimum operations needed to build this value
20 */
21 private int findMinOperations(int value) {
22 // Base case: when base >= value, we can either:
23 // 1. Add value times (x/x): costs 2*value - 1 operations
24 // 2. Subtract from base: x - (base - value) * (x/x): costs 2*(base - value) operations
25 if (base >= value) {
26 int additionCost = value * 2 - 1; // Using x/x repeated value times
27 int subtractionCost = 2 * (base - value); // x minus (base-value) times x/x
28 return Math.min(additionCost, subtractionCost);
29 }
30
31 // Check memoization cache
32 if (memo.containsKey(value)) {
33 return memo.get(value);
34 }
35
36 // Find the smallest power of base that is >= value
37 int exponent = 2; // Start with x^2 (since x^1 < value from base case)
38 long powerOfBase = (long) base * base;
39 while (powerOfBase < value) {
40 powerOfBase *= base;
41 exponent++;
42 }
43
44 // Option 1: Use the previous power and add the remainder
45 // Cost: (exponent - 1) for the power + cost of building the remainder
46 int previousPower = (int) (powerOfBase / base);
47 int remainder = value - previousPower;
48 int minOperations = exponent - 1 + findMinOperations(remainder);
49
50 // Option 2: Use current power and subtract the excess (if beneficial)
51 // Only consider if the excess is less than value (to avoid infinite recursion)
52 int excess = (int) powerOfBase - value;
53 if (excess < value) {
54 int subtractOption = exponent + findMinOperations(excess);
55 minOperations = Math.min(minOperations, subtractOption);
56 }
57
58 // Store result in cache and return
59 memo.put(value, minOperations);
60 return minOperations;
61 }
62}
63
1class Solution {
2public:
3 int leastOpsExpressTarget(int x, int target) {
4 // Memoization cache to store computed results for each value
5 unordered_map<int, int> memo;
6
7 // DFS function to find minimum operations needed to reach value v
8 function<int(int)> dfs = [&](int currentValue) -> int {
9 // Base case: when x >= currentValue
10 if (x >= currentValue) {
11 // Two options:
12 // 1. Use x/x (one division) currentValue times, needs (currentValue * 2 - 1) operations
13 // Example: if currentValue = 3, we need x/x + x/x + x/x = 3 * 2 - 1 = 5 ops (3 divisions + 2 additions)
14 // 2. Use x once and subtract (x - currentValue) times x/x, needs 2 * (x - currentValue) operations
15 // Example: x - (x - currentValue) * (x/x)
16 return min(currentValue * 2 - 1, 2 * (x - currentValue));
17 }
18
19 // Check if we've already computed this value
20 if (memo.count(currentValue)) {
21 return memo[currentValue];
22 }
23
24 // Find the smallest power of x that is >= currentValue
25 int exponent = 2; // Start with x^2 (since x^1 = x was handled above)
26 long long powerOfX = x * x;
27 while (powerOfX < currentValue) {
28 powerOfX *= x;
29 exponent++;
30 }
31
32 // Option 1: Use the previous power (powerOfX / x) and add the difference
33 // Operations needed: (exponent - 1) for the power + operations for the difference
34 int minOperations = exponent - 1 + dfs(currentValue - powerOfX / x);
35
36 // Option 2: Use the current power and subtract the excess (if it's beneficial)
37 // Only consider this if the excess is less than currentValue
38 if (powerOfX - currentValue < currentValue) {
39 minOperations = min(minOperations, exponent + dfs(powerOfX - currentValue));
40 }
41
42 // Store the result in memoization cache
43 memo[currentValue] = minOperations;
44 return minOperations;
45 };
46
47 // Start DFS from the target value
48 return dfs(target);
49 }
50};
51
1/**
2 * Finds the minimum number of operations needed to build the target value
3 * using expression with number x and operators +, -, *, /
4 *
5 * @param x - The base number to use in expressions
6 * @param target - The target value to build
7 * @returns The minimum number of operations needed
8 */
9function leastOpsExpressTarget(x: number, target: number): number {
10 // Memoization map to store computed results for each value
11 const memo: Map<number, number> = new Map();
12
13 /**
14 * Depth-first search to find minimum operations for a given value
15 *
16 * @param currentValue - The current value to process
17 * @returns Minimum number of operations needed for currentValue
18 */
19 const dfs = (currentValue: number): number => {
20 // Base case: when x is greater than current value
21 // We can either use x/x (which equals 1) multiple times, or use x - x/x
22 if (x > currentValue) {
23 // Option 1: Use currentValue number of (x/x), each needs 2 ops except first one
24 // Option 2: Use x minus (x - currentValue) number of (x/x)
25 return Math.min(currentValue * 2 - 1, 2 * (x - currentValue));
26 }
27
28 // Check if we've already computed this value
29 if (memo.has(currentValue)) {
30 return memo.get(currentValue)!;
31 }
32
33 // Find the smallest power of x that is greater than or equal to currentValue
34 let exponent = 2;
35 let powerOfX = x * x;
36 while (powerOfX < currentValue) {
37 powerOfX *= x;
38 exponent++;
39 }
40
41 // Option 1: Use the previous power and add the difference
42 // Previous power is powerOfX / x, we need (exponent - 1) operations for it
43 // Then recursively solve for the remainder
44 let minOperations = exponent - 1 + dfs(currentValue - Math.floor(powerOfX / x));
45
46 // Option 2: Use current power and subtract the excess
47 // Only consider this if the excess is less than currentValue (optimization)
48 if (powerOfX - currentValue < currentValue) {
49 minOperations = Math.min(minOperations, exponent + dfs(powerOfX - currentValue));
50 }
51
52 // Store the result in memoization map
53 memo.set(currentValue, minOperations);
54 return minOperations;
55 };
56
57 // Start the DFS with the target value
58 return dfs(target);
59}
60
Time and Space Complexity
Time Complexity: O(log(target) * log(target))
or O(log²(target))
The recursion depth is bounded by O(log(target))
because at each recursive call, we're working with values that are differences between powers of x
and the current value v
. The value v
gets reduced significantly in each recursive step - either by subtracting a power of x
close to it, or by working with x^k - v
where x^k
is the smallest power greater than v
.
At each recursion level, the while loop to find the appropriate power k
takes O(log(v))
time in the worst case, where v ≤ target
. Since we're using memoization with @cache
, each unique value of v
is computed only once.
The number of unique subproblems is bounded by O(target)
in the worst case, but due to the nature of the problem where we work with powers and differences, the actual number of unique states visited is much smaller, approximately O(log(target))
.
Therefore, the overall time complexity is O(log(target) * log(target))
= O(log²(target))
.
Space Complexity: O(log(target))
The space complexity consists of two components:
- The recursion stack depth:
O(log(target))
- as the recursion depth is logarithmic in the target value - The memoization cache:
O(log(target))
- storing results for the unique subproblems encountered
The dominant factor is O(log(target))
for both the recursion stack and the cache storage.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Integer Overflow with Large Powers
When computing x ** power
for large values of x
and power
, the result can quickly exceed Python's practical memory limits or cause performance issues. For instance, if x = 999
and we're looking for a large target, computing 999^10
would result in an astronomically large number.
Solution: Add an early termination condition to prevent unnecessary large power calculations:
@cache
def dfs(current_value: int) -> int:
if x >= current_value:
return min(current_value * 2 - 1, 2 * (x - current_value))
power = 2
power_value = x * x # Start with x^2
# Add upper bound check to prevent overflow
while power_value < current_value and power < 40: # 40 is a reasonable upper bound
power += 1
power_value *= x
if power_value > 10**9: # Safeguard against extremely large values
break
# Rest of the logic remains the same
2. Incorrect Base Case Calculation
A common mistake is misunderstanding how operators are counted in the base case. When building a value using ones (x/x), developers often forget that:
- The first
x/x
uses 1 operator (division) - Each additional
x/x
needs 1 division operator PLUS 1 addition operator to connect it
Incorrect implementation:
# Wrong: Forgetting the addition operators between terms option_add_ones = current_value # This only counts divisions!
Correct implementation:
# Right: Count both divisions and additions option_add_ones = current_value * 2 - 1 # v divisions + (v-1) additions
3. Off-by-One Error in Power Calculation
The recursive case needs k-1
operators to build x^k
(not k
operators), since x
itself requires no operators. This is a subtle but critical distinction.
Incorrect:
# Wrong: Counting x as needing 1 operator option_overshoot = power + 1 + dfs(x ** power - current_value)
Correct:
# Right: x^k needs k-1 multiplications option_overshoot = power - 1 + 1 + dfs(x ** power - current_value) # Simplified to: option_overshoot = power + dfs(x ** power - current_value)
4. Missing Edge Case: target = x
When target
equals x
, the answer should be 0 (no operators needed), but the base case logic might not handle this correctly if not carefully implemented.
Solution: Add an explicit check at the beginning:
def leastOpsExpressTarget(self, x: int, target: int) -> int:
if target == x:
return 0
# Rest of the implementation...
5. Inefficient Repeated Power Calculations
Computing x ** power
multiple times in the same recursive call wastes computational resources.
Inefficient:
if x ** power - current_value < current_value: option_overshoot = power + dfs(x ** power - current_value) # Recomputes x^power option_undershoot = power - 1 + dfs(current_value - x ** (power - 1)) # Recomputes x^(power-1)
Efficient:
power_value = x ** power prev_power_value = x ** (power - 1) if power_value - current_value < current_value: option_overshoot = power + dfs(power_value - current_value) option_undershoot = power - 1 + dfs(current_value - prev_power_value)
Which of these pictures shows the visit order of a depth-first search?
Recommended Readings
Memoization Prereq Backtracking problems backtracking Memoization is a fancy word for a simple concept so is the case for a lot of things we learn in school It means saving the previous function call result in a dictionary and reading from it when we do the exact same call again
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
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Want a Structured Path to Master System Design Too? Don’t Miss This!