2998. Minimum Number of Operations to Make X and Y Equal
Problem Description
You are given two positive integers x
and y
. Your goal is to transform x
into y
using the minimum number of operations.
You can perform any of these four operations on x
:
- Divide by 11: If
x
is divisible by 11, you can divide it by 11 - Divide by 5: If
x
is divisible by 5, you can divide it by 5 - Decrement: Subtract 1 from
x
- Increment: Add 1 to
x
The problem asks you to find the minimum number of operations needed to make x
equal to y
.
For example, if x = 26
and y = 1
:
- One possible path: 26 → 25 (decrement) → 5 (divide by 5) → 1 (divide by 5) = 3 operations
- Another path: 26 → 27 → 28 → ... → 1 (using decrements) = 25 operations
- The minimum would be 3 operations
The solution uses dynamic programming with memoization to explore different paths:
- When
y ≥ x
, the only way to reachy
fromx
is by incrementing, requiringy - x
operations - When
x > y
, we can either:- Use only decrements:
x - y
operations - Adjust
x
to be divisible by 5, divide, then continue recursively - Adjust
x
to be divisible by 11, divide, then continue recursively
- Use only decrements:
The key insight is that sometimes it's better to increment x
first to make it divisible by 5 or 11, then divide, rather than just decrementing all the way to y
.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: This problem can be modeled as a graph where each number is a node, and edges represent the operations (divide by 11, divide by 5, increment, decrement). We're finding the shortest path from node
x
to nodey
.
Is it a tree?
- No: The graph is not a tree because multiple paths can lead to the same number (e.g., we can reach 10 from 11 by decrementing, or from 50 by dividing by 5).
Is the problem related to directed acyclic graphs?
- No: The graph can have cycles (we can increment then decrement to return to the same number).
Is the problem related to shortest paths?
- Yes: We need to find the minimum number of operations (shortest path) to transform
x
intoy
.
Is the graph Weighted?
- No: Each operation has the same cost (1 operation), making this an unweighted graph problem.
BFS
- Yes: For unweighted shortest path problems, BFS is the optimal choice.
Conclusion: The flowchart suggests using BFS (Breadth-First Search) for finding the minimum number of operations. BFS explores all numbers reachable in 1 operation, then 2 operations, and so on, guaranteeing that when we first reach y
, we've found the shortest path.
The solution provided uses Dynamic Programming with memoization (DFS approach) which also works effectively for this problem, but BFS would be the classic graph-based approach for finding the minimum number of steps in an unweighted graph scenario.
Intuition
The key insight is recognizing that this is a shortest path problem in an implicit graph. Each number represents a state, and we want to find the minimum steps to go from state x
to state y
.
When thinking about how to transform x
to y
, we have two main scenarios:
-
When
x ≤ y
: We can only increasex
using increment operations, so the answer is simplyy - x
steps. -
When
x > y
: This is where it gets interesting. The naive approach would be to just decrementx
until we reachy
, takingx - y
steps. But we can often do better by using division operations.
The critical observation is that division by 5 or 11 can dramatically reduce a large number, potentially saving many steps. For example, going from 55 to 5 takes only 1 division operation instead of 50 decrements.
But here's the clever part: sometimes it's worth incrementing first to reach a number divisible by 5 or 11, then dividing. For instance, if x = 54
and we want to reach a smaller number, we could:
- Increment once to get 55, then divide by 11 to get 5 (2 operations total)
- Instead of decrementing many times
This leads to the recursive insight: at each state x
, we should consider:
- The direct path using only increment/decrement operations:
|x - y|
- Making
x
divisible by 5 (either by decrementingx % 5
times or incrementing5 - x % 5
times), then dividing and solving the subproblem - Making
x
divisible by 11 (either by decrementingx % 11
times or incrementing11 - x % 11
times), then dividing and solving the subproblem
We take the minimum of all these options. The memoization (@cache
) ensures we don't recalculate the same state multiple times, as different paths might lead to the same intermediate number.
The formula x % 5 + 1 + dfs(x // 5)
represents: decrement to make divisible by 5, perform the division (1 operation), then solve for the new number. Similarly, 5 - x % 5 + 1 + dfs(x // 5 + 1)
represents: increment to make divisible by 5, perform the division, then continue.
Learn more about Breadth-First Search, Memoization and Dynamic Programming patterns.
Solution Approach
The solution uses Dynamic Programming with memoization to explore all possible paths from x
to y
and find the minimum number of operations.
Implementation Details
1. Base Case:
if y >= x: return y - x
When y
is greater than or equal to x
, we can only use increment operations to reach y
, requiring exactly y - x
steps.
2. Direct Path Option:
ans = x - y
Initialize the answer with the simple approach of using only decrement operations to go from x
to y
.
3. Division by 5 Strategy:
ans = min(ans, x % 5 + 1 + dfs(x // 5))
ans = min(ans, 5 - x % 5 + 1 + dfs(x // 5 + 1))
- First option: Decrement
x
byx % 5
times to make it divisible by 5, then divide (1 operation), then recursively solve forx // 5
- Second option: Increment
x
by5 - x % 5
times to reach the next multiple of 5, then divide, then recursively solve forx // 5 + 1
4. Division by 11 Strategy:
ans = min(ans, x % 11 + 1 + dfs(x // 11))
ans = min(ans, 11 - x % 11 + 1 + dfs(x // 11 + 1))
- Similar logic as division by 5, but for multiples of 11
- Either decrement to the previous multiple or increment to the next multiple
5. Memoization:
The @cache
decorator automatically memoizes the results of dfs(x)
, preventing redundant calculations when the same state is reached through different paths.
Why This Works
The algorithm explores all reasonable paths:
- For any number
x
, we consider getting to nearby multiples of 5 and 11 (both smaller and larger) - After division, we have a smaller subproblem that we solve recursively
- The memoization ensures we don't recalculate states we've already visited
- By taking the minimum of all options, we guarantee finding the optimal path
Time Complexity
The memoized recursion visits each unique state at most once. The number of unique states is bounded by the range of numbers we explore, which is roughly O(x)
in the worst case. Each state performs constant work (checking 5 options).
Space Complexity
The space is used by the recursion stack and memoization cache, which is also O(x)
in the worst case.
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 trace through the algorithm with x = 26
and y = 1
.
Initial call: dfs(26)
with target y = 1
Since 26 > 1
, we don't hit the base case. We explore multiple options:
-
Direct decrement path:
26 - 1 = 25
operations -
Division by 5 strategy:
- Option A: Decrement to make divisible by 5
26 % 5 = 1
, so decrement once to get 25- Operations:
1
(decrement) +1
(divide) +dfs(5)
- Need to compute
dfs(5)
- Option B: Increment to next multiple of 5
5 - (26 % 5) = 4
, so increment 4 times to get 30- Operations:
4
(increments) +1
(divide) +dfs(6)
- Need to compute
dfs(6)
- Option A: Decrement to make divisible by 5
-
Division by 11 strategy:
- Option A: Decrement to make divisible by 11
26 % 11 = 4
, so decrement 4 times to get 22- Operations:
4
(decrements) +1
(divide) +dfs(2)
- Need to compute
dfs(2)
- Option B: Increment to next multiple of 11
11 - (26 % 11) = 7
, so increment 7 times to get 33- Operations:
7
(increments) +1
(divide) +dfs(3)
- Need to compute
dfs(3)
- Option A: Decrement to make divisible by 11
Computing the recursive calls:
-
dfs(5)
: Since5 > 1
:- Direct:
5 - 1 = 4
operations - Divide by 5:
0
(already divisible) +1
(divide) +dfs(1) = 0
= 1 operation - Minimum = 1
- Direct:
-
dfs(6)
: Since6 > 1
:- Direct:
6 - 1 = 5
operations - Divide by 5:
1
(decrement to 5) +1
(divide) +dfs(1) = 0
= 2 operations - Minimum = 2
- Direct:
-
dfs(2)
: Since2 > 1
:- Direct:
2 - 1 = 1
operation - Division options don't help (would need more operations)
- Minimum = 1
- Direct:
-
dfs(3)
: Since3 > 1
:- Direct:
3 - 1 = 2
operations - Division options don't help
- Minimum = 2
- Direct:
Back to dfs(26)
:
- Direct decrement:
25
operations - Divide by 5 (Option A):
1 + 1 + 1 = 3
operations ✓ - Divide by 5 (Option B):
4 + 1 + 2 = 7
operations - Divide by 11 (Option A):
4 + 1 + 1 = 6
operations - Divide by 11 (Option B):
7 + 1 + 2 = 10
operations
Result: Minimum is 3 operations
The optimal path is: 26 → 25
(decrement) → 5
(divide by 5) → 1
(divide by 5)
Solution Implementation
1class Solution:
2 def minimumOperationsToMakeEqual(self, x: int, y: int) -> int:
3 from functools import cache
4
5 @cache
6 def dfs(current_x: int) -> int:
7 # Base case: if target y is greater than or equal to current_x,
8 # we can only increment to reach y
9 if y >= current_x:
10 return y - current_x
11
12 # Option 1: Only use decrement operations to reach y
13 min_operations = current_x - y
14
15 # Option 2: Divide by 5
16 # First, decrement to make current_x divisible by 5, then divide
17 operations_to_divide_by_5 = current_x % 5 + 1 + dfs(current_x // 5)
18 min_operations = min(min_operations, operations_to_divide_by_5)
19
20 # Option 3: Increment to next multiple of 5, then divide
21 operations_to_next_multiple_of_5 = (5 - current_x % 5) + 1 + dfs(current_x // 5 + 1)
22 min_operations = min(min_operations, operations_to_next_multiple_of_5)
23
24 # Option 4: Divide by 11
25 # First, decrement to make current_x divisible by 11, then divide
26 operations_to_divide_by_11 = current_x % 11 + 1 + dfs(current_x // 11)
27 min_operations = min(min_operations, operations_to_divide_by_11)
28
29 # Option 5: Increment to next multiple of 11, then divide
30 operations_to_next_multiple_of_11 = (11 - current_x % 11) + 1 + dfs(current_x // 11 + 1)
31 min_operations = min(min_operations, operations_to_next_multiple_of_11)
32
33 return min_operations
34
35 return dfs(x)
36
1class Solution {
2 // Memoization cache to store computed results for each x value
3 private Map<Integer, Integer> memo = new HashMap<>();
4 private int targetY;
5
6 /**
7 * Finds the minimum number of operations to transform x into y.
8 * Operations allowed:
9 * 1. Increment x by 1
10 * 2. Decrement x by 1
11 * 3. Divide x by 5 (if x is divisible by 5)
12 * 4. Divide x by 11 (if x is divisible by 11)
13 *
14 * @param x the starting number
15 * @param y the target number
16 * @return minimum number of operations needed
17 */
18 public int minimumOperationsToMakeEqual(int x, int y) {
19 this.targetY = y;
20 return dfs(x);
21 }
22
23 /**
24 * Recursive DFS function with memoization to find minimum operations.
25 *
26 * @param currentX current value being processed
27 * @return minimum operations needed to reach targetY from currentX
28 */
29 private int dfs(int currentX) {
30 // Base case: if target is greater than or equal to current value,
31 // we can only increment to reach the target
32 if (targetY >= currentX) {
33 return targetY - currentX;
34 }
35
36 // Check if result is already computed and cached
37 if (memo.containsKey(currentX)) {
38 return memo.get(currentX);
39 }
40
41 // Option 1: Direct path using only increment/decrement operations
42 int minOperations = currentX - targetY;
43
44 // Option 2: Divide by 5 after adjusting remainder
45 // Decrement to make divisible by 5, then divide
46 int decrementAndDivideBy5 = currentX % 5 + 1 + dfs(currentX / 5);
47
48 // Option 3: Increment to next multiple of 5, then divide
49 int incrementAndDivideBy5 = (5 - currentX % 5) + 1 + dfs(currentX / 5 + 1);
50
51 // Option 4: Divide by 11 after adjusting remainder
52 // Decrement to make divisible by 11, then divide
53 int decrementAndDivideBy11 = currentX % 11 + 1 + dfs(currentX / 11);
54
55 // Option 5: Increment to next multiple of 11, then divide
56 int incrementAndDivideBy11 = (11 - currentX % 11) + 1 + dfs(currentX / 11 + 1);
57
58 // Find the minimum among all possible paths
59 minOperations = Math.min(minOperations,
60 Math.min(decrementAndDivideBy5,
61 Math.min(incrementAndDivideBy5,
62 Math.min(decrementAndDivideBy11, incrementAndDivideBy11))));
63
64 // Cache the result for future use
65 memo.put(currentX, minOperations);
66
67 return minOperations;
68 }
69}
70
1class Solution {
2public:
3 int minimumOperationsToMakeEqual(int x, int y) {
4 // Memoization map to store computed results for each value of x
5 unordered_map<int, int> memo;
6
7 // Recursive function to find minimum operations to transform x to y
8 function<int(int)> dfs = [&](int currentX) -> int {
9 // Base case: if y >= currentX, we can only decrement to reach y
10 if (y >= currentX) {
11 return y - currentX;
12 }
13
14 // Check if result is already computed
15 if (memo.count(currentX)) {
16 return memo[currentX];
17 }
18
19 // Option 1: Decrement currentX directly to y
20 int directDecrement = currentX - y;
21
22 // Option 2: Decrement to make divisible by 5, then divide
23 // currentX % 5 decrements needed to reach multiple of 5
24 // +1 for the division operation
25 int decrementThenDivideBy5 = currentX % 5 + 1 + dfs(currentX / 5);
26
27 // Option 3: Increment to make divisible by 5, then divide
28 // (5 - currentX % 5) increments needed to reach next multiple of 5
29 // +1 for the division operation
30 int incrementThenDivideBy5 = (5 - currentX % 5) + 1 + dfs(currentX / 5 + 1);
31
32 // Option 4: Decrement to make divisible by 11, then divide
33 // currentX % 11 decrements needed to reach multiple of 11
34 // +1 for the division operation
35 int decrementThenDivideBy11 = currentX % 11 + 1 + dfs(currentX / 11);
36
37 // Option 5: Increment to make divisible by 11, then divide
38 // (11 - currentX % 11) increments needed to reach next multiple of 11
39 // +1 for the division operation
40 int incrementThenDivideBy11 = (11 - currentX % 11) + 1 + dfs(currentX / 11 + 1);
41
42 // Store and return the minimum of all options
43 return memo[currentX] = min({directDecrement,
44 decrementThenDivideBy5,
45 incrementThenDivideBy5,
46 decrementThenDivideBy11,
47 incrementThenDivideBy11});
48 };
49
50 return dfs(x);
51 }
52};
53
1/**
2 * Finds the minimum number of operations to transform x into y
3 * Operations allowed:
4 * 1. Increment x by 1
5 * 2. Decrement x by 1
6 * 3. Divide x by 5 (if x is divisible by 5)
7 * 4. Divide x by 11 (if x is divisible by 11)
8 */
9function minimumOperationsToMakeEqual(x: number, y: number): number {
10 // Memoization map to store computed results for each value of x
11 const memo: Map<number, number> = new Map();
12
13 /**
14 * Recursive helper function to calculate minimum operations
15 * @param currentX - Current value being transformed
16 * @returns Minimum number of operations needed
17 */
18 const calculateMinOperations = (currentX: number): number => {
19 // Base case: if target y is greater than or equal to currentX,
20 // we can only increment to reach y
21 if (y >= currentX) {
22 return y - currentX;
23 }
24
25 // Check if result is already memoized
26 if (memo.has(currentX)) {
27 return memo.get(currentX)!;
28 }
29
30 // Strategy 1: Decrement to make divisible by 5, then divide
31 // Cost: (currentX % 5) decrements + 1 division + recursive call
32 const decrementThenDivideBy5 = (currentX % 5) + 1 + calculateMinOperations(Math.floor(currentX / 5));
33
34 // Strategy 2: Increment to make divisible by 5, then divide
35 // Cost: (5 - currentX % 5) increments + 1 division + recursive call
36 const incrementThenDivideBy5 = (5 - (currentX % 5)) + 1 + calculateMinOperations(Math.floor(currentX / 5) + 1);
37
38 // Strategy 3: Decrement to make divisible by 11, then divide
39 // Cost: (currentX % 11) decrements + 1 division + recursive call
40 const decrementThenDivideBy11 = (currentX % 11) + 1 + calculateMinOperations(Math.floor(currentX / 11));
41
42 // Strategy 4: Increment to make divisible by 11, then divide
43 // Cost: (11 - currentX % 11) increments + 1 division + recursive call
44 const incrementThenDivideBy11 = (11 - (currentX % 11)) + 1 + calculateMinOperations(Math.floor(currentX / 11) + 1);
45
46 // Strategy 5: Direct decrement from currentX to y
47 const directDecrement = currentX - y;
48
49 // Find the minimum among all strategies
50 const minOperations = Math.min(
51 directDecrement,
52 decrementThenDivideBy5,
53 incrementThenDivideBy5,
54 decrementThenDivideBy11,
55 incrementThenDivideBy11
56 );
57
58 // Store result in memoization map
59 memo.set(currentX, minOperations);
60
61 return minOperations;
62 };
63
64 return calculateMinOperations(x);
65}
66
Time and Space Complexity
Time Complexity: O(log x)
The algorithm uses memoized recursion (dynamic programming) to find the minimum operations. At each recursive call, we're essentially dividing x
by either 5 or 11, which reduces the problem size logarithmically.
- Each state
x
is computed at most once due to the@cache
decorator - From each state, we explore at most 4 options (two for division by 5, two for division by 11, plus the direct subtraction option)
- The number of unique states is bounded by
O(log x)
because:- When dividing by 5: after
k
divisions, we get approximatelyx / 5^k
- When dividing by 11: after
k
divisions, we get approximatelyx / 11^k
- The recursion stops when
x <= y
, so the depth is at mostlog_5(x/y)
orlog_11(x/y)
- When dividing by 5: after
- Since each state is computed once and each computation takes
O(1)
time, the overall time complexity isO(log x)
Space Complexity: O(log x)
The space complexity consists of two components:
- Recursion stack depth: The maximum depth of recursion is
O(log x)
as the value ofx
decreases exponentially with each division operation - Memoization cache: The cache stores at most
O(log x)
unique states, as each recursive call creates a unique state value betweeny
andx
Therefore, the total space complexity is O(log x)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Infinite Recursion Without Proper Base Case
Pitfall: A common mistake is not handling the case where continuously incrementing x
to find a multiple of 5 or 11 could lead to exploring states far beyond what's necessary, potentially causing stack overflow or timeout.
Example of problematic thinking:
# Without proper bounds, we might explore x values that are much larger than needed # For example, if x=10 and y=1, incrementing x to 55 (multiple of 11) # then dividing gives us 5, which is still > y=1
Solution: The current implementation handles this well by:
- Having a clear base case when
y >= current_x
- The recursion naturally terminates because division operations reduce the problem size
- The memoization prevents revisiting the same states
2. Forgetting to Account for the Division Operation Itself
Pitfall: When calculating the cost to divide by 5 or 11, developers often forget to add +1
for the division operation itself.
Incorrect:
# WRONG: Forgetting the division operation operations = current_x % 5 + dfs(current_x // 5) # Missing +1
Correct:
# CORRECT: Include the division operation operations = current_x % 5 + 1 + dfs(current_x // 5) # +1 for the division
3. Not Considering Both Directions (Increment and Decrement to Multiples)
Pitfall: Only considering decrementing to reach a multiple, missing potentially optimal paths that involve incrementing first.
Incomplete approach:
# Only checking decrement to previous multiple
min_ops = min(min_ops, current_x % 5 + 1 + dfs(current_x // 5))
# Missing: increment to next multiple might be better!
Why this matters: For x = 24
and y = 5
:
- Decrementing to 20, then dividing by 5 gives: 4 decrements + 1 division + 1 decrement = 6 operations
- Incrementing to 25, then dividing by 5 gives: 1 increment + 1 division = 2 operations (optimal!)
4. Integer Division Edge Cases
Pitfall: Not handling the increment-to-next-multiple case correctly due to integer division.
Incorrect calculation:
# WRONG: This doesn't give the next multiple after incrementing next_value = (current_x // 5) + 1 # This is just the quotient + 1, not the result after division
Correct calculation:
# CORRECT: First increment to next multiple, then divide increments_needed = 5 - current_x % 5 next_value_after_division = (current_x + increments_needed) // 5 # Or simply: current_x // 5 + 1 (when current_x % 5 != 0)
5. Missing Memoization Leading to Exponential Time Complexity
Pitfall: Without memoization, the same subproblems get solved multiple times, leading to exponential time complexity.
Without memoization:
def dfs(current_x): # No @cache decorator
# Same logic...
# This will recalculate dfs(5) many times if multiple paths lead to x=5
With memoization:
@cache # Critical for performance
def dfs(current_x):
# Each unique value of current_x is computed only once
Impact: For larger values of x
, the difference can be between milliseconds (with memoization) and timeout (without memoization).
You are given an array of intervals where intervals[i] = [start_i, end_i]
represent the start and end of the ith
interval. You need to merge all overlapping intervals and return an array of the non-overlapping intervals that cover all the intervals in the input.
Recommended Readings
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
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
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!