LeetCode 2998 - Minimum Number of Operations to Make X and Y Equal
The problem asks us to transform an integer x into another integer y using the minimum number of operations. At every step, we are allowed to perform exactly one of four actions: 1. Divide x by 11 if it is divisible by 11. 2. Divide x by 5 if it is divisible by 5. 3.
Difficulty: 🟡 Medium
Topics: Dynamic Programming, Breadth-First Search, Memoization
Solution
Problem Understanding
The problem asks us to transform an integer x into another integer y using the minimum number of operations. At every step, we are allowed to perform exactly one of four actions:
- Divide
xby11if it is divisible by11. - Divide
xby5if it is divisible by5. - Decrease
xby1. - Increase
xby1.
The goal is to determine the fewest operations required to make x == y.
This is fundamentally a shortest path problem. We can think of every integer as a node in a graph, and every allowed operation as an edge with cost 1. We want the shortest path from node x to node y.
The input consists of two positive integers x and y, both bounded by 10^4. The output is a single integer representing the minimum number of operations.
The constraints are important because they tell us brute force graph exploration is possible, but we must still avoid unnecessary exponential branching. Since values remain relatively small, a graph search or memoized dynamic programming solution becomes feasible.
Several edge cases immediately stand out:
- If
x == y, the answer is0because no operations are needed. - If
x < y, division operations are useless because they only decrease the number further. The optimal answer is simplyy - xincrements. - Sometimes increasing before dividing is optimal. For example,
54 → 55 → 5 → 1 → 2demonstrates that greedily dividing immediately is not always correct. - Sometimes decrementing before dividing is optimal, such as
26 → 25 → 5 → 1.
These cases show why a naive greedy strategy fails and why we need a systematic shortest path approach.
Approaches
Brute Force Approach
A brute force solution would explore every possible sequence of operations from x until reaching y. At each number, we could try all valid operations recursively.
This approach is correct because it eventually examines every possible path and can choose the shortest one. However, it is highly inefficient because the search tree branches repeatedly. Since increment and decrement are always available, the number of possible states grows rapidly.
Without memoization or pruning, the same intermediate numbers are recomputed many times. This leads to exponential time complexity, making the solution impractical.
Key Insight for an Optimal Solution
The important observation is that this problem has overlapping subproblems and optimal substructure.
Suppose we are currently at some number n. The minimum operations needed from n to y depends only on the best results of nearby states. This naturally suggests memoization.
A direct BFS would work because all operations cost 1, but there is an even cleaner recursive dynamic programming approach.
For a number n, we always have a fallback strategy:
- Move directly to
yusing only increments or decrements, costing|n - y|.
However, if division by 5 or 11 could help, we should consider adjusting n slightly to make it divisible.
For division by 5:
- Decrease
nuntil divisible by5, then divide. - Increase
nuntil divisible by5, then divide.
Similarly for division by 11.
This dramatically reduces branching because instead of exploring arbitrary increments and decrements, we only explore meaningful transitions around divisibility boundaries.
We memoize results so each state is solved once.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | Exponential | Exponential | Explores all operation sequences recursively |
| Optimal | O(log x) amortized | O(log x) | Memoized DFS with divisibility transitions |
Algorithm Walkthrough
Optimal Memoized DFS
- Define a recursive function
dfs(current)that returns the minimum operations needed to convertcurrentintoy. - Handle the simplest case first. If
current <= y, division no longer helps because it only decreases the number further. The best option is simply incrementing untily, which costsy - current. - Initialize the answer as
current - y. This represents the fallback strategy of repeatedly decrementing until reachingy. - Try using division by
5.
-
Compute how far
currentis from the nearest multiple of5. -
Let
remainder = current % 5. -
We can either:
-
Decrement
remaindertimes to reach a divisible number. -
Increment
5 - remaindertimes to reach the next divisible number. -
After adjustment, divide by
5and recursively solve the smaller state.
- Try using division by
11similarly.
- Compute
remainder = current % 11. - Consider both decrementing and incrementing to a divisible number.
- Divide and recurse.
- Return the minimum among all possible strategies.
- Use memoization so repeated states are computed only once.
The recursion shrinks numbers quickly through division, which keeps the state space small.
Why it works
The algorithm works because it evaluates every meaningful optimal transition from a state. Any shortest sequence must either:
- Move directly toward
yusing increments or decrements, or - Eventually use a division by
5or11.
Before division becomes possible, only a small number of increments or decrements are needed to reach divisibility. By exploring both directions around divisibility and memoizing results, we guarantee that every optimal path is considered exactly once.
Python Solution
from functools import lru_cache
class Solution:
def minimumOperationsToMakeEqual(self, x: int, y: int) -> int:
@lru_cache(None)
def dfs(current: int) -> int:
if current <= y:
return y - current
# Fallback: decrement directly to y
best = current - y
# Division by 5
remainder_5 = current % 5
# Decrease to nearest multiple of 5
best = min(
best,
remainder_5 + 1 + dfs(current // 5)
)
# Increase to next multiple of 5
if remainder_5 != 0:
best = min(
best,
(5 - remainder_5) + 1 + dfs(current // 5 + 1)
)
# Division by 11
remainder_11 = current % 11
# Decrease to nearest multiple of 11
best = min(
best,
remainder_11 + 1 + dfs(current // 11)
)
# Increase to next multiple of 11
if remainder_11 != 0:
best = min(
best,
(11 - remainder_11) + 1 + dfs(current // 11 + 1)
)
return best
return dfs(x)
The implementation uses a memoized recursive helper function named dfs. Each call computes the minimum operations required to transform a specific value into y.
The base case handles current <= y. Since division only decreases numbers, once we are below y, the only sensible operation is incrementing.
The variable best starts with the direct decrement path, which guarantees correctness even if division is not useful.
For division by 5 and 11, we consider both nearby divisible targets:
- Move downward to the nearest divisible number.
- Move upward to the next divisible number.
After adjusting, we perform the division and recursively solve the resulting smaller subproblem.
Memoization ensures each state is evaluated once, eliminating redundant work.
Go Solution
func minimumOperationsToMakeEqual(x int, y int) int {
memo := make(map[int]int)
var dfs func(int) int
min := func(a, b int) int {
if a < b {
return a
}
return b
}
dfs = func(current int) int {
if current <= y {
return y - current
}
if val, exists := memo[current]; exists {
return val
}
// Fallback: decrement directly to y
best := current - y
// Division by 5
remainder5 := current % 5
best = min(
best,
remainder5+1+dfs(current/5),
)
if remainder5 != 0 {
best = min(
best,
(5-remainder5)+1+dfs(current/5+1),
)
}
// Division by 11
remainder11 := current % 11
best = min(
best,
remainder11+1+dfs(current/11),
)
if remainder11 != 0 {
best = min(
best,
(11-remainder11)+1+dfs(current/11+1),
)
}
memo[current] = best
return best
}
return dfs(x)
}
The Go implementation follows the same recursive memoization strategy. Since Go does not have built in memoization decorators like Python's lru_cache, we explicitly maintain a map[int]int for caching computed states.
The recursive closure dfs captures memo and y. Integer arithmetic behaves safely within constraints because values never exceed small bounds around 10^4.
Worked Examples
Example 1: x = 26, y = 1
We begin with dfs(26).
| Step | Current | Action | Cost |
|---|---|---|---|
| 1 | 26 | Decrement to 25 | 1 |
| 2 | 25 | Divide by 5 → 5 | 1 |
| 3 | 5 | Divide by 5 → 1 | 1 |
Total cost = 3
The recursive evaluation checks other possibilities, but this path is minimal.
Example 2: x = 54, y = 2
| Step | Current | Action | Cost |
|---|---|---|---|
| 1 | 54 | Increment to 55 | 1 |
| 2 | 55 | Divide by 11 → 5 | 1 |
| 3 | 5 | Divide by 5 → 1 | 1 |
| 4 | 1 | Increment to 2 | 1 |
Total cost = 4
A greedy decrement strategy would take far more steps.
Example 3: x = 25, y = 30
Since x < y, division is never useful.
| Step | Current | Action | Cost |
|---|---|---|---|
| 1 | 25 | Increment | 1 |
| 2 | 26 | Increment | 1 |
| 3 | 27 | Increment | 1 |
| 4 | 28 | Increment | 1 |
| 5 | 29 | Increment | 1 |
Total cost = 5
The algorithm immediately returns 30 - 25.
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(log x) amortized | Recursive states shrink rapidly through division |
| Space | O(log x) | Memoization and recursion depth |
The recursion explores only meaningful states near multiples of 5 and 11. Since division drastically reduces numbers, the recursion tree remains shallow. Memoization prevents repeated recomputation, making the effective number of states very small.
Test Cases
sol = Solution()
# Provided examples
assert sol.minimumOperationsToMakeEqual(26, 1) == 3 # decrement then divide twice
assert sol.minimumOperationsToMakeEqual(54, 2) == 4 # increment before dividing
assert sol.minimumOperationsToMakeEqual(25, 30) == 5 # x < y
# Boundary values
assert sol.minimumOperationsToMakeEqual(1, 1) == 0 # already equal
assert sol.minimumOperationsToMakeEqual(1, 10000) == 9999 # only increments possible
assert sol.minimumOperationsToMakeEqual(10000, 1) >= 0 # upper bound
# Exact divisibility
assert sol.minimumOperationsToMakeEqual(55, 5) == 1 # direct division by 11
assert sol.minimumOperationsToMakeEqual(25, 1) == 2 # divide twice by 5
# Increment before division
assert sol.minimumOperationsToMakeEqual(54, 5) == 2 # 54 -> 55 -> 5
# Decrement before division
assert sol.minimumOperationsToMakeEqual(26, 5) == 1 # 26 -> 25 -> 5
# Already optimal with decrements
assert sol.minimumOperationsToMakeEqual(10, 7) == 3 # direct decrementing
# Mixed operations
assert sol.minimumOperationsToMakeEqual(121, 1) == 2 # divide by 11 twice
| Test | Why |
|---|---|
(26, 1) |
Validates decrement before division |
(54, 2) |
Validates increment before division |
(25, 30) |
Verifies x < y shortcut |
(1, 1) |
Already equal case |
(1, 10000) |
Large increment only path |
(55, 5) |
Direct divisibility by 11 |
(25, 1) |
Multiple chained divisions |
(54, 5) |
Incrementing to divisible state |
(10, 7) |
Direct decrement optimality |
(121, 1) |
Repeated division by 11 |
Edge Cases
When x == y
This is the simplest edge case. No operations are needed because the numbers are already equal. A buggy implementation might still recurse unnecessarily. Our solution handles this naturally because current <= y immediately returns y - current, which becomes 0.
When x < y
Division operations are never beneficial here because they only decrease the number further away from the target. The only optimal strategy is repeated incrementing. The implementation directly returns y - current for this case, avoiding unnecessary recursion.
When incrementing before division is optimal
A naive greedy solution might always divide immediately whenever possible. This fails for inputs like (54, 2), where increasing first creates a divisible number and produces a shorter path. Our implementation explicitly checks both directions around divisibility, guaranteeing that such cases are handled correctly.
When decrementing before division is optimal
Similarly, some cases benefit from reducing the number slightly before division. For (26, 1), decrementing once to 25 enables two efficient divisions. Since we explore both nearest divisible numbers, this path is correctly discovered.