1776. Car Fleet II
Problem Description
You have n
cars driving in the same direction on a single-lane road. Each car has a starting position and speed given in the array cars
, where cars[i] = [position_i, speed_i]
:
position_i
is the distance (in meters) of thei
-th car from the start of the roadspeed_i
is the speed (in meters per second) of thei
-th car- Cars are ordered by position, so
position_i < position_{i+1}
When a faster car catches up to a slower car ahead of it, they collide and form a fleet. After collision:
- The cars move together as a single unit
- The fleet moves at the speed of the slower car
Your task is to determine when each car will collide with the car directly in front of it. Return an array answer
where answer[i]
represents:
- The time (in seconds) when car
i
collides with cari+1
-1
if cari
never collides with the next car
The solution uses a monotonic stack approach, processing cars from right to left. For each car, it checks if and when it will catch up to cars ahead of it, considering that some cars ahead might have already collided with others. The stack maintains cars that could potentially be collision targets, removing those that would be "bypassed" due to earlier collisions.
The collision time between car i
and car j
(where j
is ahead) is calculated as: t = (position_j - position_i) / (speed_i - speed_j)
, which only applies when car i
is faster than car j
.
Intuition
The key insight is to process the cars from right to left (back to front). Why? Because a car can only collide with cars ahead of it, never behind. When we process from the back, we already know the collision information for all cars ahead, which helps us make decisions for the current car.
Consider what happens when car i
is faster than car j
ahead of it. Car i
will eventually catch up to car j
at time t = (position_j - position_i) / (speed_i - speed_j)
. But there's a catch - car j
might collide with another car before car i
reaches it. If car j
collides at time ans[j]
and this happens before car i
would reach j
, then car i
won't actually collide with j
directly.
This leads us to think about which cars are "reachable" for collision. If car i
would reach car j
after j
has already collided with someone else, we need to look further ahead. The car that j
collided with might be our actual collision target, or perhaps an even further car.
A monotonic stack naturally handles this scenario. As we process each car from right to left:
- We maintain a stack of potential collision targets
- For the current car, we check the top of the stack (the nearest car ahead)
- If the current car is slower or same speed, it will never catch up - we add it to the stack for future cars to consider
- If the current car is faster, we calculate when it would collide
- If this collision happens after the target car has already collided elsewhere, we pop that target from the stack and check the next one
- We continue popping until we find a valid collision or run out of targets
The stack effectively maintains a "chain" of cars where each subsequent car either doesn't collide or collides before being reached by cars behind it. This ensures we always find the correct first collision for each car.
Learn more about Stack, Math, Monotonic Stack and Heap (Priority Queue) patterns.
Solution Approach
The solution uses a monotonic stack to efficiently track potential collision targets while processing cars from right to left.
Data Structure:
stk
: A stack storing indices of cars that could be collision targetsans
: Result array initialized with-1
for all cars
Algorithm Steps:
-
Iterate from right to left (
i
fromn-1
to0
):- Start from the rightmost car since it has no car ahead to collide with
- Each car only needs to consider cars ahead of it
-
For each car
i
, check potential collisions:while stk: j = stk[-1] # Get the nearest car ahead
-
Determine if collision is possible:
- Check if car
i
is faster:cars[i][1] > cars[j][1]
- If not faster, car
i
will never catch up, so pop carj
from stack and check next
- Check if car
-
Calculate collision time if car
i
is faster:t = (cars[j][0] - cars[i][0]) / (cars[i][1] - cars[j][1])
This formula comes from solving:
position_i + speed_i * t = position_j + speed_j * t
-
Validate the collision:
- Check if
ans[j] == -1
(carj
doesn't collide with anyone) - OR check if
t <= ans[j]
(cari
reachesj
beforej
collides with another car) - If valid, set
ans[i] = t
and break - If invalid, pop car
j
from stack (it's unreachable) and continue checking
- Check if
-
Add current car to stack:
stk.append(i)
After processing, add car
i
to the stack as a potential target for cars behind it
Why this works:
- The stack maintains cars in increasing order of "reachability"
- When we pop a car from the stack, it means it's "blocked" by its own collision and won't be the direct collision target
- The first valid collision we find is guaranteed to be the actual collision for car
i
- Cars that never collide remain
-1
in the answer array
Time Complexity: O(n)
- each car is pushed and popped from the stack at most once
Space Complexity: O(n)
- for the stack and answer array
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 walk through a small example with 4 cars:
cars = [[0, 2], [4, 1], [8, 3], [10, 1]]
Visual representation:
Position: 0 4 8 10 Speed: 2 1 3 1 Car: 0 1 2 3
Processing from right to left:
Step 1: Process car 3 (position=10, speed=1)
- Stack is empty, no car ahead
ans[3] = -1
- Push 3 to stack:
stk = [3]
Step 2: Process car 2 (position=8, speed=3)
- Stack has car 3 (position=10, speed=1)
- Car 2 is faster (3 > 1), can catch up
- Collision time:
t = (10-8)/(3-1) = 1 second
- Car 3 doesn't collide with anyone (
ans[3] = -1
), so this collision is valid ans[2] = 1.0
- Push 2 to stack:
stk = [3, 2]
Step 3: Process car 1 (position=4, speed=1)
- Stack top is car 2 (position=8, speed=3)
- Car 1 is slower (1 < 3), will never catch up
- Pop car 2:
stk = [3]
- Check car 3 (position=10, speed=1)
- Same speed (1 = 1), will never catch up
- Pop car 3:
stk = []
- No more cars to check
ans[1] = -1
- Push 1 to stack:
stk = [1]
Step 4: Process car 0 (position=0, speed=2)
- Stack has car 1 (position=4, speed=1)
- Car 0 is faster (2 > 1), can catch up
- Collision time:
t = (4-0)/(2-1) = 4 seconds
- Car 1 doesn't collide with anyone (
ans[1] = -1
), so this collision is valid ans[0] = 4.0
- Push 0 to stack:
stk = [1, 0]
Final answer: [4.0, -1, 1.0, -1]
Interpretation:
- Car 0 collides with car 1 after 4 seconds
- Car 1 never collides (moves at same speed as car 3, slower than car 2)
- Car 2 collides with car 3 after 1 second
- Car 3 has no car ahead to collide with
The key insight demonstrated here is how car 1, despite being between cars 0 and 2, never collides with anyone because it's too slow to catch car 3 and car 2 (which is faster) collides with car 3 first.
Solution Implementation
1class Solution:
2 def getCollisionTimes(self, cars: List[List[int]]) -> List[float]:
3 # Stack to maintain cars that could potentially collide with current car
4 stack = []
5 n = len(cars)
6 # Initialize result array with -1 (no collision)
7 result = [-1] * n
8
9 # Process cars from right to left (last car to first car)
10 for i in range(n - 1, -1, -1):
11 # Check potential collisions with cars ahead (in the stack)
12 while stack:
13 j = stack[-1]
14
15 # Current car is faster than car j ahead
16 if cars[i][1] > cars[j][1]:
17 # Calculate collision time between car i and car j
18 collision_time = (cars[j][0] - cars[i][0]) / (cars[i][1] - cars[j][1])
19
20 # Valid collision if car j doesn't collide before or collides after this time
21 if result[j] == -1 or collision_time <= result[j]:
22 result[i] = collision_time
23 break
24
25 # Remove car j from stack as it won't collide with cars before i
26 stack.pop()
27
28 # Add current car to stack for processing previous cars
29 stack.append(i)
30
31 return result
32
1class Solution {
2 public double[] getCollisionTimes(int[][] cars) {
3 int n = cars.length;
4 double[] answer = new double[n];
5 // Initialize all collision times to -1 (no collision)
6 Arrays.fill(answer, -1.0);
7
8 // Stack to maintain indices of cars that could potentially collide
9 Deque<Integer> stack = new ArrayDeque<>();
10
11 // Process cars from right to left
12 for (int i = n - 1; i >= 0; i--) {
13 // Check potential collisions with cars ahead
14 while (!stack.isEmpty()) {
15 int j = stack.peek();
16
17 // Check if current car is faster than the car ahead
18 if (cars[i][1] > cars[j][1]) {
19 // Calculate collision time between car i and car j
20 double collisionTime = (double)(cars[j][0] - cars[i][0]) / (cars[i][1] - cars[j][1]);
21
22 // Check if this collision happens before car j collides with another car
23 // If car j doesn't collide with anyone (answer[j] < 0) or
24 // if collision happens before car j's collision time
25 if (answer[j] < 0 || collisionTime <= answer[j]) {
26 answer[i] = collisionTime;
27 break;
28 }
29 }
30 // Remove car j from stack as it won't affect cars to the left of i
31 stack.pop();
32 }
33 // Add current car to the stack for future consideration
34 stack.push(i);
35 }
36
37 return answer;
38 }
39}
40
1class Solution {
2public:
3 vector<double> getCollisionTimes(vector<vector<int>>& cars) {
4 int n = cars.size();
5 vector<double> result(n, -1.0); // Initialize result array with -1.0 (no collision)
6 stack<int> monotonic_stack; // Stack to maintain indices of cars in decreasing speed order
7
8 // Process cars from right to left (back to front)
9 for (int i = n - 1; i >= 0; --i) {
10 // Check potential collisions with cars ahead
11 while (!monotonic_stack.empty()) {
12 int j = monotonic_stack.top(); // Index of the car ahead
13
14 // Check if current car is faster than the car ahead
15 if (cars[i][1] > cars[j][1]) {
16 // Calculate collision time between car i and car j
17 double collision_time = static_cast<double>(cars[j][0] - cars[i][0]) /
18 (cars[i][1] - cars[j][1]);
19
20 // Check if this collision happens before car j collides with another car
21 // If car j doesn't collide with anyone (result[j] < 0) or
22 // if collision happens before car j's collision time
23 if (result[j] < 0 || collision_time <= result[j]) {
24 result[i] = collision_time;
25 break; // Found the collision time for car i
26 }
27 }
28 // Remove car j from stack as it won't affect cars further to the left
29 monotonic_stack.pop();
30 }
31
32 // Add current car to the stack for processing cars to its left
33 monotonic_stack.push(i);
34 }
35
36 return result;
37 }
38};
39
1function getCollisionTimes(cars: number[][]): number[] {
2 const n = cars.length;
3 const result: number[] = new Array(n).fill(-1.0); // Initialize result array with -1.0 (no collision)
4 const monotonicStack: number[] = []; // Stack to maintain indices of cars in decreasing speed order
5
6 // Process cars from right to left (back to front)
7 for (let i = n - 1; i >= 0; i--) {
8 // Check potential collisions with cars ahead
9 while (monotonicStack.length > 0) {
10 const j = monotonicStack[monotonicStack.length - 1]; // Index of the car ahead
11
12 // Check if current car is faster than the car ahead
13 if (cars[i][1] > cars[j][1]) {
14 // Calculate collision time between car i and car j
15 const collisionTime = (cars[j][0] - cars[i][0]) /
16 (cars[i][1] - cars[j][1]);
17
18 // Check if this collision happens before car j collides with another car
19 // If car j doesn't collide with anyone (result[j] < 0) or
20 // if collision happens before car j's collision time
21 if (result[j] < 0 || collisionTime <= result[j]) {
22 result[i] = collisionTime;
23 break; // Found the collision time for car i
24 }
25 }
26 // Remove car j from stack as it won't affect cars further to the left
27 monotonicStack.pop();
28 }
29
30 // Add current car to the stack for processing cars to its left
31 monotonicStack.push(i);
32 }
33
34 return result;
35}
36
Time and Space Complexity
Time Complexity: O(n)
The algorithm iterates through all n
cars once from right to left. Although there's a nested while loop that may pop elements from the stack, each car index is pushed onto the stack exactly once and popped at most once throughout the entire execution. This means the total number of operations across all iterations is bounded by 2n
(each element can be pushed once and popped once), resulting in an amortized O(1)
time per car and O(n)
overall time complexity.
Space Complexity: O(n)
The algorithm uses:
- A stack
stk
that can contain at mostn
elements (in the worst case, all car indices could be on the stack simultaneously) - An answer array
ans
of sizen
to store the collision times
Both data structures require O(n)
space, so the total space complexity is O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Collision Logic - Not Removing Unreachable Cars
The Pitfall: A common mistake is to check only if the current car is faster than the car ahead, without considering whether the car ahead might collide with another car before being reached. This leads to incorrect collision times.
Incorrect Implementation:
# WRONG: Only checks speed difference
for i in range(n - 1, -1, -1):
while stack:
j = stack[-1]
if cars[i][1] > cars[j][1]:
collision_time = (cars[j][0] - cars[i][0]) / (cars[i][1] - cars[j][1])
result[i] = collision_time
break # Wrong! Doesn't validate if this collision actually happens
stack.pop()
stack.append(i)
The Problem:
Car j
might collide with another car at time t1
, but car i
might calculate it reaches car j
at time t2 > t1
. In reality, car i
can't collide with car j
after j
has already collided with another car.
Correct Solution:
# Check if the collision happens before car j collides with another car if result[j] == -1 or collision_time <= result[j]: result[i] = collision_time break else: # Car j will collide before car i reaches it, so remove it stack.pop()
2. Division by Zero - Cars with Same Speed
The Pitfall: When two cars have the same speed, the collision time formula divides by zero:
collision_time = (cars[j][0] - cars[i][0]) / (cars[i][1] - cars[j][1]) # If cars[i][1] == cars[j][1], this causes division by zero!
Correct Solution: Always check that the current car is strictly faster before calculating collision time:
if cars[i][1] > cars[j][1]: # Ensures denominator is positive collision_time = (cars[j][0] - cars[i][0]) / (cars[i][1] - cars[j][1])
3. Stack Management - Forgetting to Pop Invalid Targets
The Pitfall: Not removing cars from the stack when they're unreachable can cause the algorithm to get stuck or produce wrong results.
Incorrect Pattern:
# WRONG: Breaks without popping unreachable cars while stack: j = stack[-1] if cars[i][1] <= cars[j][1]: break # Wrong! Should pop and continue
Correct Solution:
while stack: j = stack[-1] if cars[i][1] <= cars[j][1]: stack.pop() # Remove car j as it's not reachable continue # Check next car in stack
4. Processing Order - Iterating Left to Right
The Pitfall: Processing cars from left to right makes it impossible to know future collisions, leading to complex dependency chains.
Why Right to Left Works:
- The rightmost car has no car ahead, so its collision time is definitively
-1
- Each car only needs information about cars ahead (already processed)
- This creates a clean dependency order without circular references
Example to Illustrate:
# Cars: [[1,2], [3,1], [5,3]] # Processing right to left: # - Car at position 5: no collision (-1) # - Car at position 3: slower than car ahead, no collision (-1) # - Car at position 1: catches car at position 3 at time 1.0
Which of the following problems can be solved with backtracking (select multiple)
Recommended Readings
Stack Intro Imagine you have a pile of books on your desk If you want to add a new book you place it on top If you want to read a book you take it from the top And if you simply want to see which book is on the top you
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
Monotonic Stack Deque Intro If you'd prefer a video div class responsive iframe iframe src https www youtube com embed Dq_ObZwTY_Q title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div The word monotonic means a list or
Want a Structured Path to Master System Design Too? Don’t Miss This!