The Race Car problem asks you to determine the minimum number of instructions required for a car to reach a specified target position on a number line, starting from position 0
with a speed of +1
.
The car can be instructed in two ways:
4
, after accelerating, it moves 4
units and the new speed is 8
.-1
; if it was negative, it becomes +1
. The position does not change.
Your goal is to compute the minimum number of instructions (a sequence of 'A' and 'R') needed for the car to reach the exact target
position. You are guaranteed that the answer exists for any target > 0
.
At first glance, it might seem like you can just keep accelerating until you overshoot the target, then reverse and try to come back. However, the car's speed doubles with each acceleration, making it easy to overshoot or miss the target. The challenge is to find the most efficient sequence of accelerations and reverses.
A brute-force approach would try every possible sequence of instructions, but this quickly becomes infeasible as the number of possibilities grows exponentially. Therefore, we need a strategy that can prune unnecessary paths and avoid redundant calculations.
We can model the problem as a search for the shortest path in a state space, where each state is defined by the car's position and speed. This naturally leads to using Breadth-First Search (BFS), which explores all possible sequences of instructions of length n
before moving to length n+1
.
To solve the Race Car problem efficiently, we use a Breadth-First Search (BFS) approach:
(position, speed)
.(0, 1)
and 0
instructions used.(position + speed, speed * 2)
.0
and 2 * target
), since going too far away from the target is never optimal.BFS guarantees that the first time we reach the target, it is with the minimal number of instructions.
Let's walk through an example where target = 3
:
0
, speed 1
.
1
, speed 2
.
The minimal sequence is ["A", "A"]
. However, for targets where overshooting occurs, the algorithm explores all possibilities using BFS, ensuring the shortest sequence is found.
For example, for target = 6
:
Brute-force approach: Tries all possible instruction sequences, leading to exponential time and space complexity, which is infeasible for large targets.
BFS approach:
2 * target
.O(target * log(target))
(since speed doubles, the number of distinct speeds is logarithmic in target).O(target * log(target))
O(target * log(target))
for the queue and visited set.The Race Car problem is a classic example of searching for the shortest path in a state space. By representing each state as a combination of position and speed, and using BFS to explore all possible sequences of moves, we efficiently find the minimal number of instructions to reach the target. The key insight is to avoid redundant states and unnecessary paths, ensuring the solution is both correct and optimal.
from collections import deque
class Solution:
def racecar(self, target: int) -> int:
queue = deque()
visited = set()
queue.append((0, 1, 0)) # position, speed, steps
visited.add((0, 1))
while queue:
position, speed, steps = queue.popleft()
if position == target:
return steps
# Accelerate
next_pos = position + speed
next_speed = speed * 2
if (next_pos, next_speed) not in visited and 0 <= next_pos < 2 * target:
visited.add((next_pos, next_speed))
queue.append((next_pos, next_speed, steps + 1))
# Reverse
rev_speed = -1 if speed > 0 else 1
if (position, rev_speed) not in visited:
visited.add((position, rev_speed))
queue.append((position, rev_speed, steps + 1))
#include <queue>
#include <unordered_set>
#include <tuple>
class Solution {
public:
int racecar(int target) {
using State = std::tuple<int, int>;
std::queue<std::tuple<int, int, int>> q;
std::unordered_set<std::string> visited;
q.push({0, 1, 0});
visited.insert("0,1");
while (!q.empty()) {
auto [pos, speed, steps] = q.front(); q.pop();
if (pos == target) return steps;
// Accelerate
int next_pos = pos + speed;
int next_speed = speed * 2;
std::string key1 = std::to_string(next_pos) + "," + std::to_string(next_speed);
if (0 <= next_pos && next_pos < 2 * target && visited.find(key1) == visited.end()) {
visited.insert(key1);
q.push({next_pos, next_speed, steps + 1});
}
// Reverse
int rev_speed = speed > 0 ? -1 : 1;
std::string key2 = std::to_string(pos) + "," + std::to_string(rev_speed);
if (visited.find(key2) == visited.end()) {
visited.insert(key2);
q.push({pos, rev_speed, steps + 1});
}
}
return -1;
}
};
import java.util.*;
class Solution {
public int racecar(int target) {
Queue<int[]> queue = new LinkedList<>();
Set<String> visited = new HashSet<>();
queue.offer(new int[]{0, 1, 0}); // position, speed, steps
visited.add("0,1");
while (!queue.isEmpty()) {
int[] curr = queue.poll();
int pos = curr[0], speed = curr[1], steps = curr[2];
if (pos == target) return steps;
// Accelerate
int nextPos = pos + speed;
int nextSpeed = speed * 2;
String key1 = nextPos + "," + nextSpeed;
if (0 <= nextPos && nextPos < 2 * target && !visited.contains(key1)) {
visited.add(key1);
queue.offer(new int[]{nextPos, nextSpeed, steps + 1});
}
// Reverse
int revSpeed = speed > 0 ? -1 : 1;
String key2 = pos + "," + revSpeed;
if (!visited.contains(key2)) {
visited.add(key2);
queue.offer(new int[]{pos, revSpeed, steps + 1});
}
}
return -1;
}
}
var racecar = function(target) {
let queue = [];
let visited = new Set();
queue.push([0, 1, 0]); // position, speed, steps
visited.add('0,1');
while (queue.length > 0) {
let [pos, speed, steps] = queue.shift();
if (pos === target) return steps;
// Accelerate
let nextPos = pos + speed;
let nextSpeed = speed * 2;
let key1 = nextPos + ',' + nextSpeed;
if (0 <= nextPos && nextPos < 2 * target && !visited.has(key1)) {
visited.add(key1);
queue.push([nextPos, nextSpeed, steps + 1]);
}
// Reverse
let revSpeed = speed > 0 ? -1 : 1;
let key2 = pos + ',' + revSpeed;
if (!visited.has(key2)) {
visited.add(key2);
queue.push([pos, revSpeed, steps + 1]);
}
}
return -1;
};