Car Fleet II
HardPrompt
There are n cars traveling at different speeds in the same direction along a one-lane road. You are given an array cars of length n, where cars[i] = [positioni, speedi] represents:
positioniis the distance between theithcar and the beginning of the road in meters. It is guaranteed thatpositioni < positioni+1.speediis the initial speed of theithcar in meters per second.
For simplicity, cars can be considered as points moving along the number line. Two cars collide when they occupy the same position. Once a car collides with another car, they unite and form a single car fleet. The cars in the formed fleet will have the same position and the same speed, which is the initial speed of the slowest car in the fleet.
Return an array answer, where answer[i] is the time, in seconds, at which the ith car collides with the next car, or -1 if the car does not collide with the next car. Answers within 10-5 of the actual answers are accepted.
Example 1:
Input: cars = [[1,2],[2,1],[4,3],[7,2]]
Output: [1.00000,-1.00000,3.00000,-1.00000]
Explanation: After exactly one second, the first car will collide with the second car, and form a car fleet with speed 1 m/s. After exactly 3 seconds, the third car will collide with the fourth car, and form a car fleet with speed 2 m/s.Example 2:
Input: cars = [[3,4],[5,4],[6,3],[9,1]]
Output: [2.00000,1.00000,1.50000,-1.00000]
Constraints:
1 <= cars.length <= 1051 <= positioni, speedi <= 106positioni < positioni+1
Approaches
2 approaches with complexity analysis and trade-offs.
This approach iterates through each car from right to left. For each car i, it checks every car j in front of it to find the earliest possible collision time. A collision between car i and car j is only considered valid if car j does not collide with another car before car i can reach it. Since we process cars from right to left, the collision time for car j is already computed, which simplifies the logic.
Algorithm
- Initialize an
answerarray of sizenwith-1.0. - Iterate through the cars from
i = n - 2down to0. - For each car
i, initialize its minimum collision timemin_timeto a sentinel value like -1. - Iterate through the cars
jfromi + 1ton - 1. - If car
iis slower than or has the same speed as carj(cars[i][1] <= cars[j][1]), it can never catch up, so we continue to the nextj. - Otherwise, calculate the time
t_ijfor carito collide with carj:t_ij = (pos_j - pos_i) / (speed_i - speed_j). - This collision is only possible if car
jdoesn't collide with another car first. The time for carjto collide isanswer[j], which is already computed. - If
answer[j]is not-1andt_ijis greater than or equal toanswer[j], it means carjmerges into a fleet before carican hit it. So, this is not a direct collision withj. We ignore thist_ijand continue. - If the collision is possible (
answer[j] == -1ort_ij < answer[j]), thent_ijis a candidate for the collision time of cari. We updatemin_timeto be the minimum of itself andt_ij. - After checking all
j, ifmin_timewas updated,answer[i]is set tomin_time. Otherwise, it remains-1.
Walkthrough
The core idea is to solve the problem for each car i by looking ahead at all cars j > i. We iterate from the second to last car backwards to the first car. For each car i, we iterate forward from i+1 to the end. We calculate the time t_ij for i to catch j. A crucial observation is that this collision is only the first collision for i if two conditions are met: 1) i does not collide with any car k between i and j earlier than t_ij, and 2) car j does not collide with any car ahead of it before t_ij. The first condition is handled by finding the minimum time among all valid t_ij. The second condition is handled by using the pre-computed answer[j]. If t_ij is greater than answer[j], it means j is already part of a fleet by the time i gets there, so i cannot collide with j directly.
import java.util.Arrays; class Solution { public double[] getCollisionTimes(int[][] cars) { int n = cars.length; double[] answer = new double[n]; Arrays.fill(answer, -1.0); for (int i = n - 2; i >= 0; i--) { double minTime = -1.0; for (int j = i + 1; j < n; j++) { // If car i is not faster than car j, it can't catch up if (cars[i][1] <= cars[j][1]) { continue; } // Calculate time to collide double timeToCollide = (double)(cars[j][0] - cars[i][0]) / (cars[i][1] - cars[j][1]); // Check if car j collides with another car before car i collides with j if (answer[j] != -1 && timeToCollide >= answer[j]) { continue; } // If this is the first possible collision or an earlier one if (minTime == -1.0 || timeToCollide < minTime) { minTime = timeToCollide; } } answer[i] = minTime; } return answer; }}Complexity
Time
O(N^2) due to the nested loops. For each car, we potentially iterate through all cars in front of it.
Space
O(N) to store the `answer` array.
Trade-offs
Pros
Relatively straightforward to conceptualize compared to more optimized solutions.
Correctly handles the chain reaction of collisions by using previously computed results from the right-to-left scan.
Cons
The O(N^2) time complexity is too slow for the given constraints (
n <= 10^5) and will result in a 'Time Limit Exceeded' error on most platforms.
A more efficient approach uses a monotonic stack. By processing cars from right to left, we can maintain a stack of car indices that are potential collision targets. The stack helps to quickly discard cars that are either too fast to be caught or will be involved in an earlier collision, thus avoiding the O(N^2) search of the brute-force method. This optimization reduces the time complexity to linear.
Algorithm
- Initialize an
answerarray of sizenwith-1.0. - Initialize an empty stack to store car indices.
- Iterate through the cars from
i = n - 1down to0. - For the current car
i, we look at the cars on the stack. The stack will hold indices of carsjsuch thatpos_i < pos_j. - While the stack is not empty, let
jbe the index at the top of the stack.- Case 1: Car
iis slower or same speed. Ifcars[i][1] <= cars[j][1], carican never catch carj. Sincejis the closest car on the stack,ialso can't catch any carkthat is behindjon the road (and thus belowjin the stack). Carinow acts as a new 'slow car barrier' for any cars behind it. So, we popjfrom the stack as it's no longer a candidate forior any car behindi. - Case 2: Car
iis faster. Ifcars[i][1] > cars[j][1], carican potentially collide with carj. Calculate the collision timet_ij.- We must check if
jcollides with its own target beforeireaches it. The collision time forjisanswer[j], which we have already computed. - If
answer[j]is not -1 andt_ij >= answer[j], it meansjmerges into a fleet beforeican collide with it. Fromi's perspective,jis no longer a direct target. The real target is the fleetjjoins. This is equivalent to ignoringjand considering the next car on the stack. So, we popj. - If
answer[j] == -1ort_ij < answer[j], theniwill successfully collide withj. This is the first cariwill hit. We setanswer[i] = t_ijand break the while loop.
- We must check if
- Case 1: Car
- If the while loop finishes and the stack is empty, it means car
iwill not collide with any car in front of it.answer[i]remains-1.0. - Push the current index
ionto the stack. It becomes a potential collision target for the cars behind it.
Walkthrough
This approach iterates from right to left, calculating the collision time for each car. A stack is used to keep track of the indices of cars ahead that are potential collision candidates. When considering car i, we compare it with the car j at the top of the stack.
If car i is slower than or has the same speed as car j, i can't catch j. Since the stack maintains cars in increasing order of position, i also can't catch any car shielded by j. Thus, j is popped.
If car i is faster than j, we calculate the time t_ij for them to collide. We then check if this collision is realistic. If car j is already set to collide with another car at a time answer[j] that is earlier than t_ij, then car i won't collide with j but with the fleet j forms. In this case, j is not the direct target, so we pop it and check the next car on the stack.
If i can collide with j (either because j never collides or i reaches it first), we've found the answer for i, record the time, and break the inner loop. Finally, we push i onto the stack, making it a potential target for cars behind it.
import java.util.Stack; class Solution { public double[] getCollisionTimes(int[][] cars) { int n = cars.length; double[] answer = new double[n]; Stack<Integer> stack = new Stack<>(); for (int i = n - 1; i >= 0; i--) { answer[i] = -1.0; int pos_i = cars[i][0]; int speed_i = cars[i][1]; while (!stack.isEmpty()) { int j = stack.peek(); int pos_j = cars[j][0]; int speed_j = cars[j][1]; // If car i is slower or same speed as car j, i can't catch j. // Since cars on stack are ordered by position, i also can't catch // any car behind j on the road. Pop j. if (speed_i <= speed_j) { stack.pop(); continue; } // If car i is faster, calculate collision time. double timeToCollide = (double)(pos_j - pos_i) / (speed_i - speed_j); // If j collides with its next car sooner than i collides with j, // then i will actually collide with the fleet j forms. // So, j is not the direct collision target. Pop j and check the next car. if (answer[j] != -1 && timeToCollide >= answer[j]) { stack.pop(); continue; } // Otherwise, i collides with j. This is the answer for i. answer[i] = timeToCollide; break; } stack.push(i); } return answer; }}Complexity
Time
O(N). Each car index is pushed onto and popped from the stack at most once. The main loop runs `N` times, and the inner while loop operations are amortized O(1) over all iterations.
Space
O(N). In the worst case, the stack can hold all `N` indices. We also need O(N) space for the `answer` array.
Trade-offs
Pros
Highly efficient with linear time complexity, which easily passes the given constraints.
Elegantly handles the complex collision logic by maintaining a monotonic stack of candidates.
Cons
The logic can be less intuitive to grasp compared to the brute-force approach, particularly the conditions for popping from the stack.
Solutions
Solution
class Solution {public double[] getCollisionTimes(int[][] cars) { int n = cars.length; double[] ans = new double[n]; Arrays.fill(ans, -1.0); Deque<Integer> stk = new ArrayDeque<>(); for (int i = n - 1; i >= 0; --i) { while (!stk.isEmpty()) { int j = stk.peek(); if (cars[i][1] > cars[j][1]) { double t = (cars[j][0] - cars[i][0]) * 1.0 / (cars[i][1] - cars[j][1]); if (ans[j] < 0 || t <= ans[j]) { ans[i] = t; break; } } stk.pop(); } stk.push(i); } return ans; }}class Solution {public: vector<double> getCollisionTimes(vector<vector<int>> &cars) { int n = cars.size(); vector<double> ans(n, -1.0); stack<int> stk; for (int i = n - 1; ~i; --i) { while (stk.size()) { int j = stk.top(); if (cars[i][1] > cars[j][1]) { double t = (cars[j][0] - cars[i][0]) * 1.0 / (cars[i][1] - cars[j][1]); if (ans[j] < 0 || t <= ans[j]) { ans[i] = t; break; } } stk.pop(); } stk.push(i); } return ans; }};class Solution: def getCollisionTimes(self, cars: List[List[int]]) -> List[float]: stk = [] n = len(cars) ans = [- 1] * n for i in range(n - 1, - 1, - 1): while stk: j = stk[- 1] if cars[i][1] > cars[j][1]: t = (cars[j][0] - cars[i][0]) / (cars[i][1] - cars[j][1]) if ans[j] == - 1 or t <= ans[j]: ans[i] = t break stk . pop() stk . append(i) return ansVideo walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.