Facebook Pixel

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:

  1. Divide by 11: If x is divisible by 11, you can divide it by 11
  2. Divide by 5: If x is divisible by 5, you can divide it by 5
  3. Decrement: Subtract 1 from x
  4. 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 reach y from x is by incrementing, requiring y - 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

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 node y.

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 into y.

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.

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

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:

  1. When x ≤ y: We can only increase x using increment operations, so the answer is simply y - x steps.

  2. When x > y: This is where it gets interesting. The naive approach would be to just decrement x until we reach y, taking x - 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 decrementing x % 5 times or incrementing 5 - x % 5 times), then dividing and solving the subproblem
  • Making x divisible by 11 (either by decrementing x % 11 times or incrementing 11 - 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 by x % 5 times to make it divisible by 5, then divide (1 operation), then recursively solve for x // 5
  • Second option: Increment x by 5 - x % 5 times to reach the next multiple of 5, then divide, then recursively solve for x // 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 Evaluator

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

  1. Direct decrement path: 26 - 1 = 25 operations

  2. 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)
  3. 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)

Computing the recursive calls:

  • dfs(5): Since 5 > 1:

    • Direct: 5 - 1 = 4 operations
    • Divide by 5: 0 (already divisible) + 1 (divide) + dfs(1) = 0 = 1 operation
    • Minimum = 1
  • dfs(6): Since 6 > 1:

    • Direct: 6 - 1 = 5 operations
    • Divide by 5: 1 (decrement to 5) + 1 (divide) + dfs(1) = 0 = 2 operations
    • Minimum = 2
  • dfs(2): Since 2 > 1:

    • Direct: 2 - 1 = 1 operation
    • Division options don't help (would need more operations)
    • Minimum = 1
  • dfs(3): Since 3 > 1:

    • Direct: 3 - 1 = 2 operations
    • Division options don't help
    • Minimum = 2

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 approximately x / 5^k
    • When dividing by 11: after k divisions, we get approximately x / 11^k
    • The recursion stops when x <= y, so the depth is at most log_5(x/y) or log_11(x/y)
  • Since each state is computed once and each computation takes O(1) time, the overall time complexity is O(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 of x 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 between y and x

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

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

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

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

Load More