1776. Car Fleet II


Problem Description

The problem presents a scenario where n cars are moving on a single-lane road, all going the same direction. Each car has a starting position and speed, with higher-indexed cars being further down the road. The cars are simplified to points moving along a line, and when they collide, they form a single fleet with the speed of the slowest car involved in the collision. Our task is to determine the time each car takes to collide with the one in front, or -1 if no collision will occur. The answer needs to be within an error margin of 10^-5 of the actual value.

Intuition

To solve this problem, we use a stack to keep track of the cars whose collision times we need to calculate. We analyze the cars from the end of the array to the beginning, because a car's collision time will possibly affect only the cars behind it.

  1. We initiate an empty stack and an array ans filled with -1, since initially, we assume that no car will collide with the car in front.
  2. We iterate over the cars in reverse order:
    • If our stack is non-empty, we compare the current car's speed with the speed of the last car in the stack (which represents the nearest car in front with a collision time already computed, or yet to be computed).
    • If the speed of the current car is greater than the last car in the stack, a collision could occur. We then calculate the time it would take for the current car to collide with the last car in the stack.
    • If this collision time is less than or equal to the collision time of the last car in the stack (or the last car in the stack has no forthcoming collisions (ans[j] is -1)), we set this time as the answer for the current car's collision time, because it will collide before the car in the stack does or the car in the stack won't collide at all.
    • If the current car does not have a greater speed or if the collision time is greater than the collision time of the last car in the stack, we pop the last car from the stack because it will no longer collide with any previous cars.
  3. After evaluating collision times, we add the index of the current car to the stack for the consideration of the following cars.

By the end of the iteration, the ans array will have the collision times for each car or -1 if no collision occurs.

Learn more about Stack, Math, Monotonic Stack and Heap (Priority Queue) patterns.

Not Sure What to Study? Take the 2-min Quiz to Find Your Missing Piece:

A person thinks of a number between 1 and 1000. You may ask any number questions to them, provided that the question can be answered with either "yes" or "no".

What is the minimum number of questions you needed to ask so that you are guaranteed to know the number that the person is thinking?

Solution Approach

The solution uses a stack data structure to efficiently keep track of the cars and their potential collisions with the car directly in front of them. Below is a step-by-step walkthrough of the implementation detailing the algorithms, data structures, and patterns utilized:

  1. Initialization: A stack stk is initialized to an empty list to keep track of car indices whose collision times need to be determined. An array ans is also initialized with -1 for each of the n cars, indicating each car's collision time with the next car in front (initially set to -1 as we start assuming that there will be no collision).

    1stk = []
    2n = len(cars)
    3ans = [-1] * n
  2. Processing Cars in Reverse: The cars are processed from the end to the beginning to handle the propagation of collision times backward. This is done using a for loop that iterates in reverse:

    1for i in range(n - 1, -1, -1):
  3. Collision Time Calculation: Inside the loop, while there are still cars in the stack, the current car i is compared with the last car j in the stack, whether a collision is possible:

    1j = stk[-1]
    2if cars[i][1] > cars[j][1]:

    If cars[i][1], the speed of the current car, is greater than that of car j, we calculate the time t at which car i would collide with car j using the relative speeds and positions:

    1t = (cars[j][0] - cars[i][0]) / (cars[i][1] - cars[j][1])
  4. Potential Collision Validation: Once a potential collision time t is calculated, we confirm if t is a valid collision time. The collision is considered valid if:

    • Car j has no collision (indicated by ans[j] which is -1), or
    • The collision with car i happens before car j would collide with another car.
    1if ans[j] == -1 or t <= ans[j]:
    2    ans[i] = t
    3    break

    If the collision time is not valid, the car j is removed from the stack as its collision time is no longer relevant to the cars behind it.

    1stk.pop()
  5. Update Stack: After processing potential collisions for car i, we add car i to the stack to potentially collide with the next cars.

    1stk.append(i)
  6. Returning the Result: After the loop completes, the ans array, which contains the collision times for each car or -1 if the car doesn't collide, is finally returned:

    1return ans

The use of the stack along with the reverse iteration of cars allows for efficient tracking and updating of collision times, ensuring each car's collision time is correctly calculated based on the conditions outlined in the problem.

Discover Your Strengths and Weaknesses: Take Our 2-Minute Quiz to Tailor Your Study Plan:

Which algorithm is best for finding the shortest distance between two points in an unweighted graph?

Example Walkthrough

Let's use a small example with n = 3 cars to illustrate the solution approach.

Suppose we have the following cars array, where each pair consists of the position and speed of the car:

1cars = [[3, 4], [2, 5], [1, 3]]

Here, the first car (index 2) is at position 1 with speed 3, the second car (index 1) is at position 2 with speed 5, and the third car (index 0) is at position 3 with speed 4.

Now, let's walk through the algorithm:

  1. Initialization: We initialize the stk and ans arrays as empty and filled with -1, respectively.
1stk = []
2ans = [-1, -1, -1]
  1. Processing Cars in Reverse: We start with the last car (index 0) and move to the first car.
1for i in range(2, -1, -1):
  1. Collision Time Calculation: Car index 0 has no one to collide with, so we add it to the stack and move to index 1.

For car index 1 (speed 5) and car index 0 in the stack (speed 4), calculate the time duration before a collision might occur:

1t = (3 - 2) / (5 - 4) = 1.0

Since car index 0 has no previous collision (ans[0] is -1), we update ans[1] with t:

1ans = [-1, 1.0, -1]

Car index 1 is then added to the stack.

  1. Potential Collision Validation: Moving to car index 2 (speed 3), we check for a collision with car index 1 (speed 5). Since car index 2 is slower, no update to ans is needed, and car index 2 is not added to the stack, as it will never catch up. The last stack pop operation:
1stk.pop()
  1. Update Stack: After processing potential collisions for car index 2, we add car index 2 to the stack, as it is still under consideration for car index 1:
1stk.append(2)
  1. Returning the Result: The loop completes, and we return ans:
1return [-1, 1.0, -1]

This indicates that the car at index 1 takes 1.0 time units to collide with the car at index 0, and the other two cars do not collide with the car in front.

This example illustrates the algorithm's approach, efficiently determining the collision times of cars that are simply moving points on a line.

Solution Implementation

1from typing import List
2
3class Solution:
4    def get_collision_times(self, cars: List[List[int]]) -> List[float]:
5        # Stack to keep track of cars indices that we're examining
6        stack = []
7        # The number of cars
8        num_cars = len(cars)
9        # Answer list initialized with -1, indicating
10        # that initially we assume no collisions for each car
11        collision_times = [-1] * num_cars
12      
13        # We iterate from the last car to the first car
14        for i in range(num_cars - 1, -1, -1):
15            # Resolve collisions in stack
16            while stack:
17                # Get the index of the last car in the stack
18                j = stack[-1]
19                # Check if the current car is faster than the car at the top of the stack
20                if cars[i][1] > cars[j][1]:
21                    # Calculate time taken to collide with the car in front
22                    time_to_collide = (cars[j][0] - cars[i][0]) / (cars[i][1] - cars[j][1])
23                    # If the car ahead has not collided or will not collide
24                    # sooner than it takes for i to reach it, then we record this
25                    # collision time for car i and stop looking for collisions
26                    if collision_times[j] == -1 or time_to_collide <= collision_times[j]:
27                        collision_times[i] = time_to_collide
28                        break
29                # If we do not find a collision or the current car is slower
30                # than the last car in the stack, we should remove the last car
31                # from consideration
32                stack.pop()
33          
34            # Add the index of this car into the stack for potential
35            # collision calculations with further preceding cars
36            stack.append(i)
37      
38        # Return the list of times when each car would collide
39        return collision_times
40
41# Example of usage:
42# sol = Solution()
43# print(sol.get_collision_times([[1, 2], [2, 1], [4, 3]]))
44
1class Solution {
2    public double[] getCollisionTimes(int[][] cars) {
3        int numCars = cars.length; // Number of cars
4        double[] collisionTimes = new double[numCars]; // Array to store the collision times
5        // Initialize collision times with -1.0, indicating no collision
6        Arrays.fill(collisionTimes, -1.0);
7        Deque<Integer> stack = new ArrayDeque<>(); // Stack to keep track of cars that have not collided yet
8
9        // Traverse the cars array in reverse
10        for (int i = numCars - 1; i >= 0; --i) {
11            // Keep checking cars until we find a collision or run out of cars to check
12            while (!stack.isEmpty()) {
13                int nextCarIndex = stack.peek(); // Get the index of the next car in the stack
14              
15                // If the current car is faster than the next car, calculate the collision time
16                if (cars[i][1] > cars[nextCarIndex][1]) {
17                    double timeUntilCollision = (double)(cars[nextCarIndex][0] - cars[i][0]) / (cars[i][1] - cars[nextCarIndex][1]);
18                    // Only record the collision if it happens before the next car collides with another car
19                    if (collisionTimes[nextCarIndex] < 0 || timeUntilCollision <= collisionTimes[nextCarIndex]) {
20                        collisionTimes[i] = timeUntilCollision;
21                        break;
22                    }
23                }
24                stack.pop(); // Remove the next car from the stack as there won't be a collision with the current car
25            }
26            stack.push(i); // Add the current car to the stack
27        }
28        return collisionTimes; // Return the array containing the collision times for each car
29    }
30}
31
1#include <vector>
2#include <stack>
3
4class Solution {
5public:
6    vector<double> getCollisionTimes(vector<vector<int>>& cars) {
7        int numCars = cars.size();                     // numCars holds the number of cars
8        vector<double> collisionTimes(numCars, -1.0);  // Pre-fill the result with -1.0, indicating no collision
9        stack<int> carIndices;                         // Stack to hold indices of cars which might collide
10
11        // Iterate over the cars starting from the last one to the first
12        for (int currentCar = numCars - 1; currentCar >= 0; --currentCar) {
13            // Continue checking for potential collisions
14            while (!carIndices.empty()) {
15                int leadingCar = carIndices.top(); // Index of the leading car
16
17                // Check if the current car is faster than the leading car
18                if (cars[currentCar][1] > cars[leadingCar][1]) {
19                    // Calculate the time of the collision
20                    double timeToCollision = static_cast<double>(cars[leadingCar][0] - cars[currentCar][0]) / 
21                        (cars[currentCar][1] - cars[leadingCar][1]);
22
23                    // If the leading car has no other collision or the calculated collision
24                    // happens before the leading car's collision, update the result for the current car
25                    if (collisionTimes[leadingCar] < 0 || timeToCollision <= collisionTimes[leadingCar]) {
26                        collisionTimes[currentCar] = timeToCollision;
27                        break;
28                    }
29                }
30                // If the current car will not collide with the leading car, pop it from the stack
31                carIndices.pop();
32            }
33            // Push the current car index into the stack for the future potential collisions
34            carIndices.push(currentCar);
35        }
36        return collisionTimes; // Return the times at which each car will collide
37    }
38};
39
1// Define the car type as an array with two elements: position and speed of the car.
2type Car = [number, number];
3
4// Calculate collision times for an array of cars given their positions and speeds.
5function getCollisionTimes(cars: Car[]): number[] {
6    const numCars: number = cars.length;                            // The number of cars
7    const collisionTimes: number[] = new Array(numCars).fill(-1.0); // Initialize collision times with -1.0 to represent no collision
8    const carIndices: number[] = [];                               // Stack to hold the indices of cars which might collide
9
10    // Iterate through the cars from the last one to the first.
11    for (let currentCar = numCars - 1; currentCar >= 0; --currentCar) {
12        // While there are cars that may potentially collide.
13        while (carIndices.length > 0) {
14            const leadingCar = carIndices[carIndices.length - 1]; // Get the index of the car in front
15
16            // If the current car is faster than the car in front, check for potential collision.
17            if (cars[currentCar][1] > cars[leadingCar][1]) {
18                // Calculate the time of collision between the current car and the car in front.
19                const timeToCollision: number = (cars[leadingCar][0] - cars[currentCar][0]) /
20                                                (cars[currentCar][1] - cars[leadingCar][1]);
21
22                // Check if this is a valid collision time (before any collision the leading car might have).
23                if (collisionTimes[leadingCar] === -1.0 || timeToCollision <= collisionTimes[leadingCar]) {
24                    collisionTimes[currentCar] = timeToCollision;
25                    break;
26                }
27            }
28
29            // The current car won't collide with the leading car (either slower or collides later), remove leading car index from stack.
30            carIndices.pop();
31        }
32
33        // Add the current car index onto the stack to be considered for future collisions.
34        carIndices.push(currentCar);
35    }
36  
37    return collisionTimes; // Return the calculated times at which each car will collide
38}
39
40// Example usage:
41// const cars: Car[] = [[1, 2], [2, 1], [4, 3]];
42// const result = getCollisionTimes(cars);
43// console.log(result); // This would print out the collision times for each car.
44
Not Sure What to Study? Take the 2-min Quiz:

What are the two properties the problem needs to have for dynamic programming to be applicable? (Select 2)

Time and Space Complexity

The given Python code uses a decreasing stack to solve the problem of finding the collision times of cars on a single lane road where each car is represented as a (position, speed) pair.

Time Complexity: The time complexity of the code is O(n), where n is the number of cars. Each car is processed exactly once when iterating from the end of the list to the beginning. When a new car is processed, it either (1) becomes the new stack top and potentially removes several cars from the stack, or (2) finds a collision time and does not affect the stack. Since each car is pushed onto the stack and popped from the stack at most once, the total operations on the stack are linear with respect to the number of cars.

Space Complexity: The space complexity is also O(n). The stack used in the algorithm can potentially store all the cars simultaneously if none of the cars is able to collide with the car in front of it (i.e., each car is slower than the one in front of it). The ans list, which stores collision times for each car, also takes up n space.

In summary, the algorithm efficiently processes each car exactly once and manages collisions using a stack structure that maintains cars that have the potential to collide in the future.

Learn more about how to find time and space complexity quickly using problem constraints.

Fast Track Your Learning with Our Quick Skills Quiz:

Depth first search can be used to find whether two components in a graph are connected.


Recommended Readings


Got a question? Ask the Teaching Assistant anything you don't understand.

Still not clear? Ask in the Forum,  Discord or Submit the part you don't understand to our editors.


TA 👨‍🏫