Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

818. Race Car - Leetcode Solution

Problem Description

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:

  • Accelerate (A): The car moves forward by its current speed, and its speed doubles. For example, if the speed is 4, after accelerating, it moves 4 units and the new speed is 8.
  • Reverse (R): The car changes its direction. If the speed was positive, it becomes -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.

Thought Process

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.

Solution Approach

To solve the Race Car problem efficiently, we use a Breadth-First Search (BFS) approach:

  1. State Definition:
    • Each state is a tuple: (position, speed).
  2. Queue Initialization:
    • Start with the initial state (0, 1) and 0 instructions used.
  3. BFS Traversal:
    • At each step, for the current state, consider two possible actions:
      • Accelerate (A): Move to (position + speed, speed * 2).
      • Reverse (R): Change speed direction: If speed > 0, set speed to -1; if speed < 0, set speed to +1. Position remains the same.
    • Add new states to the queue if they have not been visited before. This avoids repeating the same state and ensures efficiency.
  4. Goal Check:
    • If the position equals the target, return the number of instructions used so far.
  5. Pruning:
    • To avoid unnecessary exploration, only consider positions within a reasonable range (e.g., between 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.

Example Walkthrough

Let's walk through an example where target = 3:

  1. Step 1: Start at position 0, speed 1.
    Instruction: A → position = 1, speed = 2
  2. Step 2: At position 1, speed 2.
    Instruction: A → position = 3, speed = 4
    We have reached the target!

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:

  • Accelerate 3 times: 0→1 (speed 2), 1→3 (speed 4), 3→7 (speed 8, overshoots)
  • Reverse, then accelerate backward to reach 6.
  • BFS explores all such paths and finds the shortest one.

Time and Space Complexity

Brute-force approach: Tries all possible instruction sequences, leading to exponential time and space complexity, which is infeasible for large targets.

BFS approach:

  • Each state is defined by (position, speed), and we only consider positions up to about 2 * target.
  • The number of possible states is O(target * log(target)) (since speed doubles, the number of distinct speeds is logarithmic in target).
  • Time Complexity: O(target * log(target))
  • Space Complexity: O(target * log(target)) for the queue and visited set.

Summary

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.

Code Implementation

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;
};