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 equals 3
  • 3 + 3 + 3 which equals 9
  • 3 * 3 - 3 / 3 which equals 8

The expression must follow these rules:

  1. Division returns rational numbers (not just integers)
  2. No parentheses are allowed anywhere in the expression
  3. Standard order of operations applies (multiplication and division before addition and subtraction)
  4. Unary negation is not allowed (you cannot write -x, only operations like x - 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.

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

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 operators
  • x + x = 2x requires 1 operator
  • x - x = 0 requires 1 operator
  • x * x = x² requires 1 operator
  • x / 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 requires k-1 operators

We can also create 1 using x / x (1 operator), and from there build any small integer by adding 1s 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:

  1. If v is small (less than or equal to x), we can either:

    • Build it using 1s: x/x + x/x + ... (requires 2v - 1 operators total)
    • Or subtract from x: x - (something) where we recursively find how to build x - v
  2. If v is large, we find the appropriate power of x:

    • Find k such that x^k is close to v
    • 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))

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 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

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:

  1. Build using ones: Create v by adding 1s together. Since each 1 is created using x/x (1 operator), and we need v of them with v-1 addition operators between them, the total is 2v - 1 operators.

    • Example: If x = 5 and v = 3, we get 5/5 + 5/5 + 5/5 using 5 operators total.
  2. Subtract from x: Express v as x - (x - v). This uses 1 subtraction operator plus the operators needed to create x - v. The total is 2 * (x - v) operators (since creating x - v using ones would need 2(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:

  1. Find the appropriate power: The algorithm finds the smallest k such that x^k >= v. This is done through a simple loop:

    k = 2 while x**k < v:  k += 1
  2. Decision point: Once we have x^k, we can either:

    • Overshoot and subtract: Use x^k - (x^k - v) to get v. This requires k operators to build x^k (since x^k needs k-1 multiplications), plus the operators to build x^k - v.
    • Undershoot and add: Use x^(k-1) + (v - x^(k-1)) to get v. This requires k-1 operators for x^(k-1), plus the operators to build v - x^(k-1).
  3. Optimization: The code includes a smart optimization. If x^k - v < v, meaning the overshoot difference is smaller than v 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 Evaluator

Example 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:

  1. Overshoot approach: Express 10 as 27 - 17
    • We need k - 1 = 2 operators to build 3^3 (i.e., 3 * 3 * 3)
    • We need 1 subtraction operator
    • We need dfs(17) operators to build 17
  2. Undershoot approach: Express 10 as 9 + 1
    • We need k - 2 = 1 operator to build 3^2 (i.e., 3 * 3)
    • We need 1 addition operator
    • We need dfs(1) operators to build 1

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 requires 2 * (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 needs 2 + dfs(10) operators
  • Undershoot: 9 + 8 needs 1 + dfs(8) operators

For dfs(8):

  • Since 3 < 8, we find 3^2 = 9 > 8, so k = 2
  • Overshoot: 9 - 1 needs 1 + dfs(1) = 1 + 1 = 2 operators
  • Since we only need the minimum and 9 - 8 = 1 < 8, this gives us dfs(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):

  1. Overshoot: 2 + dfs(17) = 2 + 3 = 5 operators
  2. 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.

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:

  1. The recursion stack depth: O(log(target)) - as the recursion depth is logarithmic in the target value
  2. 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)
Loading...
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What's the output of running the following function using input [30, 20, 10, 100, 33, 12]?

1def fun(arr: List[int]) -> List[int]: 2 import heapq 3 heapq.heapify(arr) 4 res = [] 5 for i in range(3): 6 res.append(heapq.heappop(arr)) 7 return res 8
1public static int[] fun(int[] arr) { 2 int[] res = new int[3]; 3 PriorityQueue<Integer> heap = new PriorityQueue<>(); 4 for (int i = 0; i < arr.length; i++) { 5 heap.add(arr[i]); 6 } 7 for (int i = 0; i < 3; i++) { 8 res[i] = heap.poll(); 9 } 10 return res; 11} 12
1class HeapItem { 2 constructor(item, priority = item) { 3 this.item = item; 4 this.priority = priority; 5 } 6} 7 8class MinHeap { 9 constructor() { 10 this.heap = []; 11 } 12 13 push(node) { 14 // insert the new node at the end of the heap array 15 this.heap.push(node); 16 // find the correct position for the new node 17 this.bubble_up(); 18 } 19 20 bubble_up() { 21 let index = this.heap.length - 1; 22 23 while (index > 0) { 24 const element = this.heap[index]; 25 const parentIndex = Math.floor((index - 1) / 2); 26 const parent = this.heap[parentIndex]; 27 28 if (parent.priority <= element.priority) break; 29 // if the parent is bigger than the child then swap the parent and child 30 this.heap[index] = parent; 31 this.heap[parentIndex] = element; 32 index = parentIndex; 33 } 34 } 35 36 pop() { 37 const min = this.heap[0]; 38 this.heap[0] = this.heap[this.size() - 1]; 39 this.heap.pop(); 40 this.bubble_down(); 41 return min; 42 } 43 44 bubble_down() { 45 let index = 0; 46 let min = index; 47 const n = this.heap.length; 48 49 while (index < n) { 50 const left = 2 * index + 1; 51 const right = left + 1; 52 53 if (left < n && this.heap[left].priority < this.heap[min].priority) { 54 min = left; 55 } 56 if (right < n && this.heap[right].priority < this.heap[min].priority) { 57 min = right; 58 } 59 if (min === index) break; 60 [this.heap[min], this.heap[index]] = [this.heap[index], this.heap[min]]; 61 index = min; 62 } 63 } 64 65 peek() { 66 return this.heap[0]; 67 } 68 69 size() { 70 return this.heap.length; 71 } 72} 73 74function fun(arr) { 75 const heap = new MinHeap(); 76 for (const x of arr) { 77 heap.push(new HeapItem(x)); 78 } 79 const res = []; 80 for (let i = 0; i < 3; i++) { 81 res.push(heap.pop().item); 82 } 83 return res; 84} 85

Recommended Readings

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

Load More