LeetCode 818 - Race Car
This problem asks us to control a car moving on an infinite one dimensional number line. The car starts at position 0 with speed +1.
Difficulty: 🔴 Hard
Topics: Dynamic Programming
Solution
LeetCode 818 - Race Car
Problem Understanding
This problem asks us to control a car moving on an infinite one dimensional number line. The car starts at position 0 with speed +1. We are allowed to issue two kinds of instructions:
"A"for accelerate"R"for reverse
The "A" instruction moves the car forward according to its current speed, then doubles the speed. If the current speed is negative, the car moves backward and the magnitude of the speed doubles.
The "R" instruction does not move the car at all. Instead, it resets the speed direction:
- Positive speed becomes
-1 - Negative speed becomes
+1
The goal is to reach exactly the target position using the fewest instructions possible.
For example, if the target is 3, the sequence "AA" works:
- Start at
(position=0, speed=1) - First
"A"→(1, 2) - Second
"A"→(3, 4)
The answer is therefore 2.
The constraints are important:
1 <= target <= 10^4
A target up to ten thousand is large enough that naive brute force exploration becomes impractical. We need a solution that avoids exploring every possible instruction sequence blindly.
An important observation is that repeated accelerations create positions of the form:
$$1, 3, 7, 15, 31, ...$$
These are:
$$2^k - 1$$
after k consecutive accelerations.
Several edge cases make this problem tricky:
- The target may already lie exactly on a position reachable with only accelerations.
- The optimal path may overshoot the target and come back.
- The optimal path may stop before the target, reverse, backtrack slightly, then continue forward again.
- The search space includes negative positions and alternating directions, which can explode combinatorially in naive solutions.
Because of these behaviors, simple greedy strategies do not work reliably.
Approaches
Brute Force Approach
The most straightforward idea is to model the problem as a graph search problem.
Each state consists of:
- Current position
- Current speed
From each state, we can generate two next states:
- Apply
"A" - Apply
"R"
We could run Breadth First Search because every instruction costs exactly one step. BFS guarantees the first time we reach the target is the shortest sequence.
This approach is correct because BFS explores states in increasing order of instruction count.
However, the state space grows extremely quickly. Positions and speeds can become very large due to repeated doubling. Even with pruning, the number of reachable states becomes enormous.
For targets near 10^4, brute force BFS becomes inefficient.
Optimal Dynamic Programming Approach
The key insight is that acceleration creates very structured movement patterns.
After k accelerations:
$$position = 2^k - 1$$
This means we can think in terms of powers of two.
For any target:
- Either we reach it exactly using only accelerations
- Or we overshoot it and reverse
- Or we stop before it, reverse briefly, then continue
This naturally leads to dynamic programming.
Define:
$$dp[t]$$
as the minimum instructions needed to reach target t.
For each target, we compute the smallest k such that:
$$2^k - 1 \ge t$$
Then we consider two possibilities:
- Overshoot the target and reverse back
- Stop before the target, reverse for some distance, then move forward again
Because every subproblem involves smaller targets, memoization or bottom up DP works efficiently.
Approach Comparison
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force BFS | Exponential | Exponential | Explores huge state space of positions and speeds |
| Optimal Dynamic Programming | O(target log target) | O(target) | Uses structure of acceleration distances |
Algorithm Walkthrough
Step 1: Define the DP State
Let dp[t] represent the minimum number of instructions required to reach position t.
We want to compute:
$$dp[target]$$
Step 2: Find the Smallest Overshooting Power
For a given target t, determine the minimum number of accelerations k such that:
$$2^k - 1 \ge t$$
This corresponds to accelerating k times continuously.
The resulting position is:
$$full = 2^k - 1$$
Step 3: Handle Exact Match
If:
$$full = t$$
then we can reach the target directly with k accelerations.
So:
$$dp[t] = k$$
Step 4: Consider Overshooting
If full > t, we may:
- Accelerate
ktimes - Reverse once
- Solve the remaining distance recursively
The remaining distance becomes:
$$full - t$$
The instruction count is:
$$k + 1 + dp[full - t]$$
where:
kinstructions for acceleration1instruction for reverse
Step 5: Consider Stopping Before the Target
Instead of overshooting, we can stop at:
$$2^{k-1} - 1$$
Then:
- Reverse
- Move backward with
maccelerations - Reverse again
- Continue toward the target
Backward movement after m accelerations covers:
$$2^m - 1$$
The remaining forward distance becomes:
$$t - (2^{k-1} - 1) + (2^m - 1)$$
The total instructions are:
$$(k - 1) + 1 + m + 1 + dp[\text{remaining}]$$
We try all valid m values and take the minimum.
Step 6: Memoize Results
Since many subproblems repeat, memoization avoids recomputation.
This reduces the complexity dramatically.
Why it works
The algorithm works because every optimal sequence must eventually fall into one of two categories:
- Overshoot the target and reverse
- Stop before the target, partially backtrack, then continue
The DP recurrence exhaustively considers both possibilities. Since each recursive call solves a strictly smaller target, memoization guarantees efficient computation and optimality.
Python Solution
class Solution:
def racecar(self, target: int) -> int:
memo = {0: 0}
def dp(t: int) -> int:
if t in memo:
return memo[t]
k = t.bit_length()
full_distance = (1 << k) - 1
# Exact match
if full_distance == t:
memo[t] = k
return k
# Case 1: Overshoot and reverse
result = k + 1 + dp(full_distance - t)
# Case 2: Stop before target, reverse, backtrack, reverse again
partial_distance = (1 << (k - 1)) - 1
for m in range(k - 1):
backward_distance = (1 << m) - 1
remaining = (
t
- partial_distance
+ backward_distance
)
instructions = (
(k - 1) # forward accelerations
+ 1 # reverse
+ m # backward accelerations
+ 1 # reverse again
+ dp(remaining)
)
result = min(result, instructions)
memo[t] = result
return result
return dp(target)
The implementation uses top down memoized dynamic programming.
The memo dictionary stores previously computed answers. This prevents exponential recomputation of overlapping subproblems.
The expression:
t.bit_length()
computes the smallest k such that:
$$2^k > t$$
which helps determine the first acceleration sequence that reaches or exceeds the target.
The variable full_distance represents the position reached after k accelerations:
(1 << k) - 1
If this exactly equals the target, then the answer is simply k.
Otherwise, the code evaluates both recurrence options:
- Overshoot and reverse
- Stop early, reverse, partially backtrack, then continue
The loop over m explores every possible backward acceleration count before turning forward again.
Finally, the minimum result is memoized and returned.
Go Solution
package main
func racecar(target int) int {
memo := map[int]int{
0: 0,
}
var dp func(int) int
dp = func(t int) int {
if val, exists := memo[t]; exists {
return val
}
k := 0
for (1<<k)-1 < t {
k++
}
fullDistance := (1 << k) - 1
// Exact match
if fullDistance == t {
memo[t] = k
return k
}
// Case 1: Overshoot and reverse
result := k + 1 + dp(fullDistance-t)
partialDistance := (1 << (k - 1)) - 1
// Case 2: Stop before target
for m := 0; m < k-1; m++ {
backwardDistance := (1 << m) - 1
remaining := t - partialDistance + backwardDistance
instructions :=
(k - 1) +
1 +
m +
1 +
dp(remaining)
if instructions < result {
result = instructions
}
}
memo[t] = result
return result
}
return dp(target)
}
The Go implementation follows the same recurrence structure as the Python version.
Since Go does not provide Python's bit_length() method, we compute k manually using a loop.
The memoization table uses a map[int]int. Recursive closures in Go require declaring the function variable before assigning the implementation.
Integer overflow is not a concern because the target constraint is only 10^4, and all intermediate values remain safely within Go's integer range.
Worked Examples
Example 1
Input:
target = 3
We compute:
| k | Position After k Accelerations |
|---|---|
| 1 | 1 |
| 2 | 3 |
Since:
$$2^2 - 1 = 3$$
we can reach the target exactly with "AA".
| Step | Instruction | Position | Speed |
|---|---|---|---|
| Start | - | 0 | 1 |
| 1 | A | 1 | 2 |
| 2 | A | 3 | 4 |
Answer:
2
Example 2
Input:
target = 6
Find smallest k:
| k | Position |
|---|---|
| 1 | 1 |
| 2 | 3 |
| 3 | 7 |
Now:
$$7 > 6$$
So we overshoot.
Sequence:
AAA
reaches position 7.
Then reverse:
AAAR
Now speed becomes -1.
One backward acceleration:
AAARA
moves from 7 to 6.
| Step | Instruction | Position | Speed |
|---|---|---|---|
| Start | - | 0 | 1 |
| 1 | A | 1 | 2 |
| 2 | A | 3 | 4 |
| 3 | A | 7 | 8 |
| 4 | R | 7 | -1 |
| 5 | A | 6 | -2 |
Answer:
5
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(target log target) | Each target explores at most logarithmically many transitions |
| Space | O(target) | Memoization table stores one result per target |
For each target t, we compute transitions involving at most O(log t) possibilities because the number of acceleration levels is logarithmic.
Since each target is memoized exactly once, the overall complexity becomes:
$$O(target \log target)$$
The memoization dictionary stores results for targets from 0 to target, giving linear space usage.
Test Cases
sol = Solution()
assert sol.racecar(1) == 1 # Single acceleration
assert sol.racecar(2) == 4 # Requires reversing
assert sol.racecar(3) == 2 # Exact power-of-two-minus-one
assert sol.racecar(4) == 5 # Overshoot then reverse
assert sol.racecar(5) == 7 # Multiple reversals
assert sol.racecar(6) == 5 # Example from prompt
assert sol.racecar(7) == 3 # Exact sequence AAA
assert sol.racecar(8) == 6 # Just beyond exact acceleration point
assert sol.racecar(9) == 8 # Requires careful backtracking
assert sol.racecar(15) == 4 # Exact power-of-two-minus-one
assert sol.racecar(16) == 7 # Slightly above exact point
assert sol.racecar(31) == 5 # Larger exact acceleration point
assert sol.racecar(100) > 0 # Larger target stress test
assert sol.racecar(1000) > 0 # Large constraint stress test
assert sol.racecar(10000) > 0 # Maximum constraint stress test
| Test | Why |
|---|---|
target = 1 |
Smallest valid input |
target = 2 |
Requires reversing early |
target = 3 |
Exact acceleration target |
target = 4 |
Simple overshoot case |
target = 5 |
Requires mixed strategy |
target = 6 |
Official example |
target = 7 |
Exact 2^k - 1 target |
target = 8 |
Just beyond perfect acceleration |
target = 9 |
More complicated DP structure |
target = 15 |
Larger exact acceleration point |
target = 16 |
Edge after exact point |
target = 31 |
Another exact acceleration point |
target = 100 |
Medium stress test |
target = 1000 |
Large stress test |
target = 10000 |
Maximum constraint validation |
Edge Cases
One important edge case occurs when the target is exactly of the form:
$$2^k - 1$$
Examples include 1, 3, 7, 15, and 31. These positions can be reached using only accelerations, without any reversals. A buggy implementation might still attempt unnecessary recursive transitions. This solution handles the case immediately by checking:
if full_distance == t:
and returning k directly.
Another important edge case happens when overshooting is optimal. For example, target 6 is most efficiently solved by reaching 7 first, then reversing once. Greedy approaches that try to avoid overshooting often fail here. The DP recurrence explicitly evaluates the overshoot scenario and guarantees it is considered.
A third tricky edge case involves partial backtracking. Some targets are best solved by stopping before the target, reversing briefly, then continuing forward again. For example, certain mid range targets require a carefully balanced combination of accelerations and reversals. The loop over all possible backward acceleration counts ensures every such strategy is evaluated correctly.
Finally, large targets near 10^4 stress inefficient recursion and repeated recomputation. Without memoization, the recursive structure would become exponential. The memo table guarantees each subproblem is solved once, keeping the runtime manageable within the problem constraints.