Facebook Pixel

2139. Minimum Moves to Reach Target Score

Problem Description

You're playing a number game where you start with the integer 1 and want to reach a given integer target.

In each move, you can perform one of two operations:

  • Increment: Add 1 to your current number (x = x + 1)
  • Double: Multiply your current number by 2 (x = 2 * x)

The increment operation can be used unlimited times, but the double operation can only be used at most maxDoubles times.

Given two integers target and maxDoubles, you need to find the minimum number of moves required to reach target starting from 1.

For example, if target = 10 and maxDoubles = 2, one possible sequence could be:

  • Start: 1
  • Double: 2
  • Double: 4
  • Increment: 5
  • Increment: 6
  • Increment: 7
  • Increment: 8
  • Increment: 9
  • Increment: 10

But there might be a more efficient path using fewer total moves.

The problem asks you to determine the optimal strategy that minimizes the total number of operations needed to reach the target value.

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

Intuition

The key insight is to work backwards from the target to 1, rather than forward from 1 to target. This makes the problem much clearer.

When working backwards, we can reverse the operations:

  • Instead of increment (x + 1), we decrement (x - 1)
  • Instead of double (x * 2), we halve (x / 2)

Starting from target, we want to reach 1 using the minimum number of moves. At each step, we need to decide whether to decrement or halve.

The greedy strategy becomes obvious when we think about it this way: halving reduces the number much faster than decrementing. For instance, going from 16 to 8 takes just 1 halving operation, but would take 8 decrement operations. So whenever possible, we should prefer halving.

When can we halve? Two conditions must be met:

  1. The current number must be even (we can't halve odd numbers to get integers)
  2. We must have remaining "halving" operations available (maxDoubles > 0)

If the current number is odd, we have no choice but to decrement it by 1 to make it even, then we can potentially halve it in the next step.

This leads to the recursive strategy:

  • Base case: If target = 1, we're done (0 moves)
  • If maxDoubles = 0, we can only decrement, so we need target - 1 moves
  • If target is even and we have doubles left, halve it (1 move + solve for target/2 with maxDoubles-1)
  • If target is odd, decrement it (1 move + solve for target-1 with same maxDoubles)

The beauty of this approach is that it naturally finds the optimal path by greedily using the more powerful operation (halving/doubling) whenever possible.

Learn more about Greedy and Math patterns.

Solution Approach

The solution implements the backtracking strategy using recursion. Let's walk through the implementation step by step:

Recursive Implementation:

def minMoves(self, target: int, maxDoubles: int) -> int:
    if target == 1:
        return 0
    if maxDoubles == 0:
        return target - 1
    if target % 2 == 0 and maxDoubles:
        return 1 + self.minMoves(target >> 1, maxDoubles - 1)
    return 1 + self.minMoves(target - 1, maxDoubles)

The function handles four cases:

  1. Base Case (target == 1): We've reached our goal. No more moves needed, return 0.

  2. No Doubles Left (maxDoubles == 0): We can only use decrement operations. To go from target to 1, we need exactly target - 1 decrements.

  3. Even Target with Doubles Available (target % 2 == 0 and maxDoubles > 0): This is where we apply the halving operation. We:

    • Use 1 move to halve the target (using bit shift target >> 1 which is equivalent to target // 2)
    • Decrement maxDoubles by 1
    • Recursively solve for the new smaller problem
    • Add 1 to account for the current move
  4. Odd Target: We can only decrement by 1. We:

    • Use 1 move to decrement (target - 1)
    • Keep maxDoubles unchanged (we didn't use a double operation)
    • Recursively solve for the new problem
    • Add 1 to account for the current move

Time and Space Complexity:

  • Time Complexity: O(min(log target, maxDoubles)). In the worst case, we either use all our doubles (bounded by maxDoubles) or keep halving until we reach 1 (bounded by log target).

  • Space Complexity: O(min(log target, maxDoubles)) due to the recursion stack depth.

Iterative Alternative:

The reference mentions we can convert this to an iterative approach to save space:

def minMoves(self, target: int, maxDoubles: int) -> int:
    moves = 0
    while target > 1:
        if maxDoubles == 0:
            return moves + target - 1
        if target % 2 == 0:
            target //= 2
            maxDoubles -= 1
        else:
            target -= 1
        moves += 1
    return moves

This iterative version maintains the same logic but uses O(1) space instead of O(min(log target, maxDoubles)) for the recursion stack.

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 solution with target = 19 and maxDoubles = 2.

We'll work backwards from 19 to 1, keeping track of our moves:

Step 1: Current = 19, maxDoubles = 2, moves = 0

  • 19 is odd, so we must decrement
  • New state: Current = 18, maxDoubles = 2, moves = 1

Step 2: Current = 18, maxDoubles = 2, moves = 1

  • 18 is even and we have doubles available, so we halve
  • New state: Current = 9, maxDoubles = 1, moves = 2

Step 3: Current = 9, maxDoubles = 1, moves = 2

  • 9 is odd, so we must decrement
  • New state: Current = 8, maxDoubles = 1, moves = 3

Step 4: Current = 8, maxDoubles = 1, moves = 3

  • 8 is even and we have doubles available, so we halve
  • New state: Current = 4, maxDoubles = 0, moves = 4

Step 5: Current = 4, maxDoubles = 0, moves = 4

  • No doubles left, so we can only decrement
  • We need 4 - 1 = 3 more decrements to reach 1
  • Total moves = 4 + 3 = 7

Verification (forward path): Starting from 1 and applying the reverse operations:

  1. Start: 1
  2. Double: 2
  3. Double: 4
  4. Increment: 5
  5. Increment: 6
  6. Increment: 7
  7. Increment: 8
  8. Increment: 9
  9. Double: 18
  10. Increment: 19

Wait, that's 10 moves forward, but we calculated 7 moves backward. Let me recalculate:

Actually, when working backwards, the operations map as:

  • 19 → 18 (decrement in reverse = increment forward)
  • 18 → 9 (halve in reverse = double forward)
  • 9 → 8 (decrement in reverse = increment forward)
  • 8 → 4 (halve in reverse = double forward)
  • 4 → 1 (3 decrements in reverse = 3 increments forward)

So the forward path from 1 to 19 is:

  1. Start: 1
  2. Increment: 2
  3. Increment: 3
  4. Increment: 4
  5. Double: 8
  6. Increment: 9
  7. Double: 18
  8. Increment: 19

Total: 7 moves, which matches our backward calculation!

Solution Implementation

1class Solution:
2    def minMoves(self, target: int, maxDoubles: int) -> int:
3        """
4        Calculate minimum number of moves to reach target from 1.
5        Two operations allowed:
6        1. Increment by 1
7        2. Double the current value (limited by maxDoubles)
8      
9        Args:
10            target: The target number to reach from 1
11            maxDoubles: Maximum number of doubling operations allowed
12      
13        Returns:
14            Minimum number of moves required
15        """
16        # Base case: already at target 1
17        if target == 1:
18            return 0
19      
20        # No doubles left: can only increment by 1
21        if maxDoubles == 0:
22            return target - 1
23      
24        # If target is even and we have doubles available, 
25        # work backwards by halving (equivalent to doubling from source)
26        if target % 2 == 0 and maxDoubles > 0:
27            return 1 + self.minMoves(target // 2, maxDoubles - 1)
28      
29        # If target is odd, we must have reached it by incrementing
30        # So work backwards by subtracting 1
31        return 1 + self.minMoves(target - 1, maxDoubles)
32
1class Solution {
2    /**
3     * Calculates the minimum number of moves to reach target from 1.
4     * Two operations are allowed:
5     * 1. Increment by 1
6     * 2. Double the current value (limited by maxDoubles)
7     * 
8     * @param target The target value to reach from 1
9     * @param maxDoubles The maximum number of doubling operations allowed
10     * @return The minimum number of moves required
11     */
12    public int minMoves(int target, int maxDoubles) {
13        // Base case: already at target value 1
14        if (target == 1) {
15            return 0;
16        }
17      
18        // No doubling operations left, can only increment by 1
19        if (maxDoubles == 0) {
20            return target - 1;
21        }
22      
23        // If target is even and we have doubling operations available,
24        // work backwards by halving the target (equivalent to doubling from smaller value)
25        if (target % 2 == 0 && maxDoubles > 0) {
26            return 1 + minMoves(target >> 1, maxDoubles - 1);
27        }
28      
29        // Target is odd, must decrement by 1 to make it even
30        return 1 + minMoves(target - 1, maxDoubles);
31    }
32}
33
1class Solution {
2public:
3    /**
4     * Calculate minimum number of moves to reach target from 1
5     * Operations allowed:
6     * 1. Increment by 1 (unlimited uses)
7     * 2. Double the current value (limited to maxDoubles uses)
8     * 
9     * @param target - The target value to reach from 1
10     * @param maxDoubles - Maximum number of doubling operations allowed
11     * @return Minimum number of moves required
12     */
13    int minMoves(int target, int maxDoubles) {
14        // Base case: already at target value 1
15        if (target == 1) {
16            return 0;
17        }
18      
19        // No doubling operations left, can only increment
20        // Need (target - 1) increments to reach target from 1
21        if (maxDoubles == 0) {
22            return target - 1;
23        }
24      
25        // If target is even and we have doubles remaining,
26        // work backwards: halve the target and use one double operation
27        if (target % 2 == 0 && maxDoubles > 0) {
28            return 1 + minMoves(target >> 1, maxDoubles - 1);
29        }
30      
31        // Target is odd, must decrement by 1 first
32        // (working backwards from target)
33        return 1 + minMoves(target - 1, maxDoubles);
34    }
35};
36
1/**
2 * Calculates the minimum number of moves to reach target from 1
3 * Available operations:
4 * 1. Increment by 1
5 * 2. Double the current value (limited by maxDoubles)
6 * 
7 * @param target - The target number to reach from 1
8 * @param maxDoubles - Maximum number of doubling operations allowed
9 * @returns The minimum number of moves required
10 */
11function minMoves(target: number, maxDoubles: number): number {
12    // Base case: already at target value of 1
13    if (target === 1) {
14        return 0;
15    }
16  
17    // No doubling operations left, can only increment by 1
18    if (maxDoubles === 0) {
19        return target - 1;
20    }
21  
22    // Target is even and we have doubling operations available
23    // Work backwards: if target is even, it could have come from target/2 via doubling
24    if (target % 2 === 0 && maxDoubles > 0) {
25        return 1 + minMoves(target >> 1, maxDoubles - 1);
26    }
27  
28    // Target is odd or no doubles left, must have come from target-1 via increment
29    return 1 + minMoves(target - 1, maxDoubles);
30}
31

Time and Space Complexity

Time Complexity: O(log(target))

The algorithm uses a greedy approach working backwards from the target to 1. At each step:

  • If the target is even and we have doubles remaining, we divide by 2 (right shift by 1)
  • Otherwise, we subtract 1

In the worst case, we can perform at most maxDoubles division operations, and each division reduces the target by half. Between divisions, we may need to perform subtraction operations. The total number of operations is bounded by O(log(target)) because:

  • We can have at most log₂(target) useful divisions
  • Between divisions, we only subtract 1 when the number is odd, which happens at most once before each division
  • Even if maxDoubles > log₂(target), we can only meaningfully use log₂(target) doubles

Space Complexity: O(log(target))

The space complexity is determined by the recursion depth. Since each recursive call either:

  • Divides the target by 2 (when even and doubles available)
  • Subtracts 1 from the target (when odd or no doubles left)

The maximum recursion depth is O(log(target)) in the worst case, as we're effectively reducing the problem size by at least half through the combination of operations. The recursion stack will therefore use O(log(target)) space.

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Misunderstanding the Backward Approach

A common misconception is trying to solve this problem forward (from 1 to target) instead of backward (from target to 1). The forward approach leads to exponential branching and is much harder to optimize.

Pitfall Example:

# Wrong approach - trying to go forward
def minMoves(self, current, target, maxDoubles):
    if current == target:
        return 0
    if current > target:
        return float('inf')
  
    # This creates exponential branches!
    increment = 1 + self.minMoves(current + 1, target, maxDoubles)
    double = float('inf')
    if maxDoubles > 0:
        double = 1 + self.minMoves(current * 2, target, maxDoubles - 1)
  
    return min(increment, double)

Solution: Work backward from target to 1. When going backward:

  • Doubling becomes halving (divide by 2)
  • Incrementing becomes decrementing (subtract 1)

This creates a linear path with clear decision points.

2. Incorrect Handling of the Greedy Choice

Some might think we should always use doubles when available, but this isn't optimal.

Pitfall Example:

# Wrong - always using doubles when possible
if maxDoubles > 0 and target > 1:
    return 1 + self.minMoves(target // 2, maxDoubles - 1)

This would incorrectly handle odd numbers by integer division, losing information.

Solution: Only use the halving operation when the target is even. For odd numbers, we must decrement first:

if target % 2 == 0 and maxDoubles > 0:
    return 1 + self.minMoves(target // 2, maxDoubles - 1)
else:
    return 1 + self.minMoves(target - 1, maxDoubles)

3. Stack Overflow for Large Inputs

The recursive solution can cause stack overflow for very large target values with limited doubles.

Pitfall Example: If target = 1000000 and maxDoubles = 0, the recursion depth would be 999,999, likely causing a stack overflow.

Solution: Use the iterative approach for production code:

def minMoves(self, target: int, maxDoubles: int) -> int:
    moves = 0
    while target > 1:
        if maxDoubles == 0:
            # Optimize: calculate remaining moves directly
            return moves + target - 1
      
        if target % 2 == 0:
            target //= 2
            maxDoubles -= 1
        else:
            target -= 1
        moves += 1
  
    return moves

4. Not Optimizing the No-Doubles-Left Case

When maxDoubles reaches 0, some implementations continue the recursion unnecessarily.

Pitfall Example:

# Inefficient - continues recursion even when only decrements are possible
if maxDoubles == 0:
    return 1 + self.minMoves(target - 1, 0)  # Unnecessary recursion

Solution: When no doubles remain, calculate the remaining moves directly:

if maxDoubles == 0:
    return target - 1  # Direct calculation

This optimization prevents unnecessary recursive calls and improves performance significantly.

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

What are the most two important steps in writing a depth first search function? (Select 2)


Recommended Readings

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

Load More