Facebook Pixel

818. Race Car

Problem Description

You have a race car on an infinite number line starting at position 0 with initial speed +1. The car can move into negative positions and operates based on two commands:

Command 'A' (Accelerate):

  • First, update position: position += speed
  • Then, double the speed: speed *= 2

Command 'R' (Reverse):

  • If current speed is positive: set speed = -1
  • If current speed is negative: set speed = 1
  • Position remains unchanged

For example, with the command sequence "AAR":

  • Start: position = 0, speed = 1
  • After 'A': position = 0 + 1 = 1, speed = 1 * 2 = 2
  • After 'A': position = 1 + 2 = 3, speed = 2 * 2 = 4
  • After 'R': position = 3 (unchanged), speed = -1 (reversed from positive)

Given a target position target, find the minimum number of commands needed to reach that exact position.

The solution uses dynamic programming where dp[i] represents the minimum number of commands to reach position i. The algorithm considers different strategies:

  • If position i equals 2^k - 1 for some k, we can reach it with exactly k accelerations
  • Otherwise, we can either overshoot and reverse back, or approach from below with a combination of accelerations and reversals

The key insight is that positions of the form 2^k - 1 (like 1, 3, 7, 15...) can be reached optimally with consecutive accelerations, and other positions require strategic use of the reverse command.

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

Intuition

The key observation is understanding the positions we can reach with consecutive accelerations. Starting from position 0 with speed 1:

  • After 1 'A': position = 1, speed = 2
  • After 2 'A's: position = 3, speed = 4
  • After 3 'A's: position = 7, speed = 8
  • After k 'A's: position = 2^k - 1, speed = 2^k

Notice that consecutive accelerations give us positions 1, 3, 7, 15, 31... which are all of the form 2^k - 1. These are the "perfect" positions that can be reached most efficiently with exactly k accelerations.

For any other target position, we have two main strategies:

Strategy 1: Overshoot and come back If our target is between 2^(k-1) - 1 and 2^k - 1, we can:

  • Accelerate k times to reach 2^k - 1 (overshooting the target)
  • Reverse to change direction (1 command)
  • Then solve the subproblem of reaching the remaining distance (2^k - 1) - target in the opposite direction

This gives us the formula: dp[target] = k + 1 + dp[2^k - 1 - target]

Strategy 2: Approach from below Alternatively, we can:

  • Accelerate k-1 times to reach 2^(k-1) - 1 (stopping short of target)
  • Reverse to change direction (1 command)
  • Accelerate j times backward to position 2^(k-1) - 1 - (2^j - 1)
  • Reverse again to go forward (1 command)
  • Solve the remaining distance to target

This gives us: dp[target] = (k-1) + 1 + j + 1 + dp[target - (2^(k-1) - 2^j)]

The optimal solution is the minimum among all these possible strategies. The bit length operation k = target.bit_length() helps us quickly find the smallest k such that 2^k > target, which determines the range of strategies we need to consider.

Learn more about Dynamic Programming patterns.

Solution Approach

The solution uses dynamic programming with a bottom-up approach. We create an array dp where dp[i] stores the minimum number of commands needed to reach position i.

Step 1: Initialize the DP array

dp = [0] * (target + 1)

We build solutions for all positions from 1 to target.

Step 2: Process each position For each position i from 1 to target:

First, calculate k = i.bit_length(), which gives us the smallest integer such that 2^k > i. This means i falls in the range [2^(k-1), 2^k - 1].

Step 3: Handle perfect positions

if i == 2**k - 1:
    dp[i] = k
    continue

If i equals 2^k - 1, we can reach it with exactly k consecutive accelerations. This is optimal, so we set dp[i] = k and move to the next position.

Step 4: Strategy 1 - Overshoot and return

dp[i] = dp[2**k - 1 - i] + k + 1

We accelerate k times to reach position 2^k - 1, then reverse (adding 1 command), and solve the subproblem of traveling (2^k - 1) - i distance in the opposite direction. The total cost is k accelerations + 1 reverse + dp[2^k - 1 - i].

Step 5: Strategy 2 - Approach from below with backtracking

for j in range(k - 1):
    dp[i] = min(dp[i], dp[i - (2**(k - 1) - 2**j)] + k - 1 + j + 2)

We try all possible ways to approach from below:

  • Accelerate k-1 times to reach 2^(k-1) - 1
  • Reverse (1 command)
  • Accelerate j times backward, moving 2^j - 1 units back
  • Reverse again (1 command)
  • Solve remaining distance: i - (2^(k-1) - 1) + (2^j - 1) = i - (2^(k-1) - 2^j)

The formula simplifies to:

  • (k-1) forward accelerations
  • 1 reverse
  • j backward accelerations
  • 1 reverse
  • dp[i - (2^(k-1) - 2^j)] for the remaining distance

Total: (k-1) + 1 + j + 1 + dp[i - (2^(k-1) - 2^j)] = k - 1 + j + 2 + dp[i - (2^(k-1) - 2^j)]

Step 6: Return the result After computing all positions up to target, return dp[target] as the minimum number of commands needed.

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 find the minimum number of commands to reach position 5.

Step 1: Build DP table from position 1 to 5

Position 1:

  • k = 1.bit_length() = 1 (since 2^1 = 2 > 1)
  • Check if 1 == 2^1 - 1 = 1: Yes! This is a perfect position
  • dp[1] = 1 (one 'A' command: pos 0→1)

Position 2:

  • k = 2.bit_length() = 2 (since 2^2 = 4 > 2)
  • Check if 2 == 2^2 - 1 = 3: No
  • Strategy 1 (overshoot): Go to 3, reverse, go back 1
    • dp[2] = k + 1 + dp[3-2] = 2 + 1 + dp[1] = 2 + 1 + 1 = 4
  • Strategy 2 (approach from below): Go to 1, reverse, go back 0 positions, reverse, go forward 1
    • For j = 0: dp[2] = (k-1) + 1 + j + 1 + dp[2-(1-0)] = 1 + 1 + 0 + 1 + dp[1] = 3 + 1 = 4
  • dp[2] = min(4, 4) = 4 (Commands: "AARR" - go to 1, reverse, reverse again, go to 2)

Position 3:

  • k = 3.bit_length() = 2 (since 2^2 = 4 > 3)
  • Check if 3 == 2^2 - 1 = 3: Yes! This is a perfect position
  • dp[3] = 2 (two 'A' commands: pos 0→1→3)

Position 4:

  • k = 4.bit_length() = 3 (since 2^3 = 8 > 4)
  • Check if 4 == 2^3 - 1 = 7: No
  • Strategy 1 (overshoot to 7): dp[4] = 3 + 1 + dp[7-4] = 3 + 1 + dp[3] = 3 + 1 + 2 = 6
  • Strategy 2 (from position 3):
    • For j = 0: Go to 3, reverse, don't go back, reverse, go 1 forward
      • dp[4] = 2 + 1 + 0 + 1 + dp[4-3] = 2 + 1 + 0 + 1 + dp[1] = 4 + 1 = 5
    • For j = 1: Go to 3, reverse, go back 1, reverse, go 2 forward
      • dp[4] = 2 + 1 + 1 + 1 + dp[4-(3-1)] = 2 + 1 + 1 + 1 + dp[2] = 5 + 4 = 9
  • dp[4] = min(6, 5, 9) = 5 (Commands: "AARRA" - go to 3, reverse twice, go to 4)

Position 5:

  • k = 5.bit_length() = 3 (since 2^3 = 8 > 5)
  • Check if 5 == 2^3 - 1 = 7: No
  • Strategy 1 (overshoot to 7): dp[5] = 3 + 1 + dp[7-5] = 3 + 1 + dp[2] = 3 + 1 + 4 = 8
  • Strategy 2 (from position 3):
    • For j = 0: dp[5] = 2 + 1 + 0 + 1 + dp[5-3] = 4 + dp[2] = 4 + 4 = 8
    • For j = 1: dp[5] = 2 + 1 + 1 + 1 + dp[5-(3-1)] = 5 + dp[3] = 5 + 2 = 7
  • dp[5] = min(8, 8, 7) = 7

Answer: 7 commands minimum to reach position 5

One optimal sequence: "AARARAA"

  • Start: pos=0, speed=1
  • A: pos=1, speed=2
  • A: pos=3, speed=4
  • R: pos=3, speed=-1
  • A: pos=2, speed=-2
  • R: pos=2, speed=1
  • A: pos=3, speed=2
  • A: pos=5, speed=4

Solution Implementation

1class Solution:
2    def racecar(self, target: int) -> int:
3        # dp[i] stores the minimum number of instructions to reach position i
4        dp = [0] * (target + 1)
5      
6        # Calculate minimum instructions for each position from 1 to target
7        for position in range(1, target + 1):
8            # Calculate the number of bits needed to represent the position
9            # This represents the minimum accelerations needed if we go straight
10            bits_needed = position.bit_length()
11          
12            # Check if position is exactly reachable by continuous acceleration
13            # (i.e., position is of the form 2^k - 1)
14            if position == 2**bits_needed - 1:
15                # Can reach this position with exactly k accelerations
16                dp[position] = bits_needed
17                continue
18          
19            # Case 1: Overshoot the target and then reverse
20            # Go to (2^k - 1), then reverse and cover the remaining distance
21            overshoot_position = 2**bits_needed - 1
22            remaining_distance = overshoot_position - position
23            dp[position] = dp[remaining_distance] + bits_needed + 1
24          
25            # Case 2: Undershoot, reverse, go back some distance, then reverse again
26            # Try different amounts of backward movement
27            for backward_steps in range(bits_needed - 1):
28                # Forward distance: 2^(k-1) - 1 (one bit less than full)
29                # Backward distance: 2^j - 1 (where j is backward_steps)
30                forward_distance = 2**(bits_needed - 1) - 1
31                backward_distance = 2**backward_steps - 1
32                net_distance = forward_distance - backward_distance
33              
34                # Calculate remaining distance to target
35                remaining = position - net_distance
36              
37                # Total instructions: (k-1) forward + 1 reverse + j backward + 1 reverse + remaining
38                instructions = (bits_needed - 1) + 1 + backward_steps + 1 + dp[remaining]
39                dp[position] = min(dp[position], instructions)
40      
41        return dp[target]
42
1class Solution {
2    public int racecar(int target) {
3        // dp[i] represents minimum instructions needed to reach position i
4        int[] dp = new int[target + 1];
5      
6        // Calculate minimum instructions for each position from 1 to target
7        for (int i = 1; i <= target; ++i) {
8            // Find the number of bits needed to represent i (position of highest bit + 1)
9            // This represents the number of 'A' instructions if we go straight past target
10            int bitsNeeded = 32 - Integer.numberOfLeadingZeros(i);
11          
12            // Check if current position is exactly reachable with consecutive 'A' instructions
13            // Position is 2^k - 1 (e.g., 1, 3, 7, 15, 31...)
14            if (i == (1 << bitsNeeded) - 1) {
15                // Can reach exactly with k consecutive 'A' instructions
16                dp[i] = bitsNeeded;
17                continue;
18            }
19          
20            // Option 1: Overshoot the target, then reverse and come back
21            // Go to position (2^k - 1), reverse, then go to target
22            // Total: k 'A' instructions + 1 'R' + instructions to go back
23            int overshootPosition = (1 << bitsNeeded) - 1;
24            int distanceBack = overshootPosition - i;
25            dp[i] = dp[distanceBack] + bitsNeeded + 1;
26          
27            // Option 2: Undershoot, reverse, go back some distance, reverse again, then continue
28            // Go to position (2^(k-1) - 1), reverse, go back (2^j - 1), reverse, continue to target
29            int undershootPosition = (1 << (bitsNeeded - 1)) - 1;
30          
31            for (int backSteps = 0; backSteps < bitsNeeded; ++backSteps) {
32                // Calculate remaining distance after undershooting and going back
33                int backDistance = (1 << backSteps) - 1;
34                int remainingDistance = i - undershootPosition + backDistance;
35              
36                // Total instructions: (k-1) 'A' + 1 'R' + j 'A' + 1 'R' + instructions for remaining
37                int instructionCount = (bitsNeeded - 1) + 1 + backSteps + 1 + dp[remainingDistance];
38              
39                // Keep the minimum instruction count
40                dp[i] = Math.min(dp[i], instructionCount);
41            }
42        }
43      
44        return dp[target];
45    }
46}
47
1class Solution {
2public:
3    int racecar(int target) {
4        // dp[i] represents the minimum number of instructions to reach position i
5        vector<int> dp(target + 1);
6      
7        for (int i = 1; i <= target; ++i) {
8            // Find the number of bits needed to represent i (position of highest bit + 1)
9            // This represents the number of accelerations needed to reach or pass position i
10            int numBits = 32 - __builtin_clz(i);
11          
12            // Check if i is exactly reachable by consecutive accelerations (2^numBits - 1)
13            // For example: positions 1, 3, 7, 15, 31... can be reached with only A instructions
14            if (i == (1 << numBits) - 1) {
15                dp[i] = numBits;
16                continue;
17            }
18          
19            // Option 1: Overshoot the target, then reverse and come back
20            // Go to position (2^numBits - 1), reverse, then reach the remaining distance
21            int overshootPosition = (1 << numBits) - 1;
22            dp[i] = dp[overshootPosition - i] + numBits + 1;  // +1 for the reverse instruction
23          
24            // Option 2: Undershoot the target, reverse briefly, reverse again, then continue
25            // Go to position (2^(numBits-1) - 1), reverse, go back a bit, reverse again, continue
26            for (int backSteps = 0; backSteps < numBits; ++backSteps) {
27                int undershootPosition = (1 << (numBits - 1)) - 1;  // Position after (numBits-1) accelerations
28                int backDistance = (1 << backSteps) - 1;            // Distance traveled backwards
29                int remainingDistance = i - undershootPosition + backDistance;
30              
31                // Total instructions: (numBits-1) forward + 1 reverse + backSteps backward + 1 reverse + remaining
32                dp[i] = min(dp[i], dp[remainingDistance] + (numBits - 1) + backSteps + 2);
33            }
34        }
35      
36        return dp[target];
37    }
38};
39
1function racecar(target: number): number {
2    // dp[i] represents the minimum number of instructions to reach position i
3    const dp: number[] = new Array(target + 1).fill(0);
4  
5    for (let i = 1; i <= target; i++) {
6        // Find the number of bits needed to represent i (position of highest bit + 1)
7        // This represents the number of accelerations needed to reach or pass position i
8        const numBits = 32 - Math.clz32(i);
9      
10        // Check if i is exactly reachable by consecutive accelerations (2^numBits - 1)
11        // For example: positions 1, 3, 7, 15, 31... can be reached with only A instructions
12        if (i === (1 << numBits) - 1) {
13            dp[i] = numBits;
14            continue;
15        }
16      
17        // Option 1: Overshoot the target, then reverse and come back
18        // Go to position (2^numBits - 1), reverse, then reach the remaining distance
19        const overshootPosition = (1 << numBits) - 1;
20        dp[i] = dp[overshootPosition - i] + numBits + 1;  // +1 for the reverse instruction
21      
22        // Option 2: Undershoot the target, reverse briefly, reverse again, then continue
23        // Go to position (2^(numBits-1) - 1), reverse, go back a bit, reverse again, continue
24        for (let backSteps = 0; backSteps < numBits; backSteps++) {
25            const undershootPosition = (1 << (numBits - 1)) - 1;  // Position after (numBits-1) accelerations
26            const backDistance = (1 << backSteps) - 1;             // Distance traveled backwards
27            const remainingDistance = i - undershootPosition + backDistance;
28          
29            // Total instructions: (numBits-1) forward + 1 reverse + backSteps backward + 1 reverse + remaining
30            dp[i] = Math.min(dp[i], dp[remainingDistance] + (numBits - 1) + backSteps + 2);
31        }
32    }
33  
34    return dp[target];
35}
36

Time and Space Complexity

Time Complexity: O(n * log n)

The algorithm uses dynamic programming to compute the minimum number of instructions to reach each position from 1 to target.

  • The outer loop iterates through all positions from 1 to target, which is O(n) where n = target.
  • For each position i:
    • Computing bit_length() takes O(log i) time.
    • If i = 2^k - 1, the computation is O(1).
    • Otherwise, there's an inner loop that runs from j = 0 to k - 2, where k ≈ log i. This inner loop performs O(log i) iterations.
    • Each iteration of the inner loop performs constant time operations (array lookups and comparisons).
  • Therefore, for each position i, the work done is O(log i).
  • The total time complexity is ∑(i=1 to n) O(log i) = O(n * log n).

Space Complexity: O(n)

The algorithm uses a single array dp of size target + 1 to store the minimum number of instructions for each position from 0 to target. No other data structures scale with the input size, so the space complexity is O(n) where n = target.

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

Common Pitfalls

1. Integer Overflow When Calculating Powers of 2

The Pitfall: When calculating 2**k for large values of k, the result can become extremely large, potentially causing memory issues or slow performance. Even though we're only interested in positions up to target, intermediate calculations like 2**bits_needed - 1 might exceed necessary bounds.

Example of the Issue:

# If target = 1000 and bits_needed = 10
# We calculate 2^10 - 1 = 1023
# But if bits_needed accidentally becomes large (due to a bug),
# 2^30 would be over 1 billion, causing unnecessary computation

Solution: Add bounds checking and early termination:

def racecar(self, target: int) -> int:
    dp = [0] * (target + 1)
  
    for position in range(1, target + 1):
        bits_needed = position.bit_length()
      
        # Early termination check
        if position == (1 << bits_needed) - 1:  # Using bit shift instead of power
            dp[position] = bits_needed
            continue
      
        # Add upper bound check for overshoot case
        overshoot_position = (1 << bits_needed) - 1
        if overshoot_position - position < target + 1:
            dp[position] = dp[overshoot_position - position] + bits_needed + 1
      
        # Process undershoot cases with bounds checking
        for backward_steps in range(bits_needed - 1):
            forward_distance = (1 << (bits_needed - 1)) - 1
            backward_distance = (1 << backward_steps) - 1
            remaining = position - (forward_distance - backward_distance)
          
            # Only consider valid remaining distances
            if 0 <= remaining <= target:
                instructions = bits_needed - 1 + backward_steps + 2 + dp[remaining]
                dp[position] = min(dp[position], instructions)
  
    return dp[target]

2. Not Initializing dp[position] Before Taking Minimum

The Pitfall: In the original code, dp[position] is first set in the overshoot case, then potentially updated with min() in the loop. If the overshoot case calculation fails or is skipped (due to bounds), dp[position] might remain 0, leading to incorrect results.

Solution: Initialize dp[position] to infinity before calculating:

for position in range(1, target + 1):
    bits_needed = position.bit_length()
  
    if position == (1 << bits_needed) - 1:
        dp[position] = bits_needed
        continue
  
    # Initialize to infinity
    dp[position] = float('inf')
  
    # Overshoot case
    overshoot_position = (1 << bits_needed) - 1
    remaining_distance = overshoot_position - position
    if remaining_distance <= target:
        dp[position] = min(dp[position], dp[remaining_distance] + bits_needed + 1)
  
    # Undershoot cases
    for backward_steps in range(bits_needed - 1):
        # ... rest of the logic

3. Array Index Out of Bounds

The Pitfall: When calculating dp[2**bits_needed - 1 - position] in the overshoot case, if 2**bits_needed - 1 - position exceeds target, we'll get an index error since our dp array only goes up to target.

Solution: Either extend the dp array size or add explicit bounds checking:

def racecar(self, target: int) -> int:
    # Extend dp array to handle overshoot cases
    max_position = 2 * target  # Conservative upper bound
    dp = [float('inf')] * (max_position + 1)
    dp[0] = 0
  
    for position in range(1, target + 1):
        bits_needed = position.bit_length()
      
        if position == (1 << bits_needed) - 1:
            dp[position] = bits_needed
            continue
      
        # Overshoot case with bounds check
        overshoot_position = (1 << bits_needed) - 1
        remaining = overshoot_position - position
        if remaining < max_position:
            dp[position] = min(dp[position], dp[remaining] + bits_needed + 1)
      
        # ... rest of the logic

These pitfalls can cause runtime errors, incorrect results, or performance issues. The key is to always validate array indices, handle edge cases, and consider the mathematical bounds of your calculations.

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

A heap is a ...?


Recommended Readings

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

Load More