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.
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:
- The current number must be even (we can't halve odd numbers to get integers)
- 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 needtarget - 1
moves - If
target
is even and we have doubles left, halve it (1 move + solve fortarget/2
withmaxDoubles-1
) - If
target
is odd, decrement it (1 move + solve fortarget-1
with samemaxDoubles
)
The beauty of this approach is that it naturally finds the optimal path by greedily using the more powerful operation (halving/doubling) whenever possible.
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:
-
Base Case (
target == 1
): We've reached our goal. No more moves needed, return0
. -
No Doubles Left (
maxDoubles == 0
): We can only use decrement operations. To go fromtarget
to1
, we need exactlytarget - 1
decrements. -
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 totarget // 2
) - Decrement
maxDoubles
by 1 - Recursively solve for the new smaller problem
- Add 1 to account for the current move
- Use 1 move to halve the target (using bit shift
-
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
- Use 1 move to decrement (
Time and Space Complexity:
-
Time Complexity:
O(min(log target, maxDoubles))
. In the worst case, we either use all our doubles (bounded bymaxDoubles
) or keep halving until we reach 1 (bounded bylog 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 EvaluatorExample 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:
- Start: 1
- Double: 2
- Double: 4
- Increment: 5
- Increment: 6
- Increment: 7
- Increment: 8
- Increment: 9
- Double: 18
- 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:
- Start: 1
- Increment: 2
- Increment: 3
- Increment: 4
- Double: 8
- Increment: 9
- Double: 18
- 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 uselog₂(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.
What are the most two important steps in writing a depth first search function? (Select 2)
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!