LeetCode 853 - Car Fleet
The problem describes a set of cars driving toward the same destination, represented by the integer target. Each car starts at a unique position and moves at a constant speed. Cars cannot overtake each other.
Difficulty: 🟡 Medium
Topics: Array, Stack, Sorting, Monotonic Stack
Solution
Problem Understanding
The problem describes a set of cars driving toward the same destination, represented by the integer target. Each car starts at a unique position and moves at a constant speed. Cars cannot overtake each other. If a faster car catches up to a slower car ahead of it, the faster car must slow down and continue traveling together with the slower car. Once this happens, they form a single car fleet.
The task is to determine how many distinct fleets will eventually arrive at the target.
The input consists of:
target, the destination position on the road.position[i], the starting location of thei-thcar.speed[i], the speed of thei-thcar.
The output is a single integer representing the number of fleets that reach the target.
A key detail is that if two cars meet exactly at the target position, they are still considered part of the same fleet. This affects comparisons when determining whether cars merge.
The constraints are important because they rule out inefficient simulations:
- Up to
10^5cars can exist. - Positions and speeds can both be large.
- All positions are unique.
Since n can be very large, any solution worse than roughly O(n log n) will likely time out. This immediately suggests that repeatedly simulating movement over time is not practical.
Several edge cases are important:
- A single car should always produce exactly one fleet.
- Cars already ordered by increasing arrival times never merge.
- Multiple cars can merge into one large fleet.
- A faster car behind a slower car may catch it exactly at the destination, and this still counts as one fleet.
- Because positions are unique, we never need to handle ties in starting locations.
Approaches
Brute Force Approach
A straightforward idea is to simulate the cars moving toward the target over time. At each moment, we could check whether one car catches another and merge them into fleets dynamically.
This approach is conceptually correct because it directly models the problem definition. However, it becomes extremely inefficient. Cars move continuously, and determining exactly when collisions happen requires repeated comparisons between cars and recalculations after merges.
Another brute-force variant is to compare every pair of cars and determine whether one catches another before the target. This requires considering interactions transitively, because fleets formed earlier affect later cars.
In the worst case, these approaches require O(n^2) comparisons or complicated simulations, which is too slow for n = 10^5.
Optimal Approach
The key insight is that we do not actually need to simulate movement.
For each car, we can compute the time required to reach the target:
$$\text{time} = \frac{target - position}{speed}$$
Now consider cars ordered from closest to the target to farthest away.
If a car behind takes less than or equal time to reach the destination than the car ahead, it must eventually catch up before or exactly at the target. Therefore, they form the same fleet.
If the car behind takes strictly longer, it can never catch the fleet ahead, so it forms a new fleet.
This observation allows us to process cars in reverse positional order while tracking the maximum arrival time seen so far. The pattern behaves like a monotonic stack, because fleet times are non-decreasing as we move backward.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(n²) | O(1) or O(n) | Simulates interactions or compares all pairs |
| Optimal | O(n log n) | O(n) | Sort by position, process arrival times greedily |
Algorithm Walkthrough
- Pair each car's position with its speed.
We need to process cars based on their location on the road. Since positions are unique, sorting gives a clear driving order. 2. Sort the cars by position in ascending order.
After sorting, the car closest to the target appears last. This ordering allows us to process cars from front to back when iterating in reverse. 3. Iterate through the cars from right to left.
We start with the car nearest the target because no car can influence it from ahead. 4. Compute the arrival time for the current car.
The formula is:
$t = \frac{target - position}{speed}$
This represents how long the car would take to reach the destination if it never slowed down. 5. Compare the current arrival time with the latest fleet arrival time.
- If the current car's time is greater, it cannot catch the fleet ahead. Therefore, it forms a new fleet.
- If the current car's time is less than or equal to the fleet ahead's time, it merges into that fleet.
- Maintain the maximum arrival time encountered so far.
This value represents the arrival time of the most recent fleet. 7. Count how many distinct fleets are formed.
Every time we encounter a larger arrival time, we increment the fleet count.
Why it works
Processing from nearest to farthest is the crucial idea. Every fleet ahead acts like a moving barrier. A car behind can only either merge into that fleet or remain separate.
If the rear car reaches the target sooner than the fleet ahead, it must catch up before arriving. Since overtaking is impossible, the rear car merges into that fleet and inherits its slower arrival time.
Therefore, the sequence of fleet arrival times is monotonically increasing when scanning backward through the sorted cars. Counting how many times the arrival time increases gives the exact number of fleets.
Python Solution
from typing import List
class Solution:
def carFleet(self, target: int, position: List[int], speed: List[int]) -> int:
cars = sorted(zip(position, speed))
fleets = 0
latest_time = 0.0
for pos, spd in reversed(cars):
arrival_time = (target - pos) / spd
if arrival_time > latest_time:
fleets += 1
latest_time = arrival_time
return fleets
The implementation starts by combining positions and speeds into pairs using zip. Sorting these pairs by position gives the cars in road order.
The loop iterates in reverse because we want to process cars from closest to the target toward the farthest. For each car, the code computes the time required to reach the target.
The variable latest_time stores the arrival time of the fleet currently ahead. If the current car would arrive later than that fleet, it cannot catch up and must form a new fleet. Otherwise, it merges into the existing fleet.
The variable fleets counts how many distinct fleets exist.
Go Solution
package main
import (
"sort"
)
func carFleet(target int, position []int, speed []int) int {
type Car struct {
position int
speed int
}
n := len(position)
cars := make([]Car, n)
for i := 0; i < n; i++ {
cars[i] = Car{
position: position[i],
speed: speed[i],
}
}
sort.Slice(cars, func(i, j int) bool {
return cars[i].position < cars[j].position
})
fleets := 0
latestTime := 0.0
for i := n - 1; i >= 0; i-- {
arrivalTime := float64(target-cars[i].position) / float64(cars[i].speed)
if arrivalTime > latestTime {
fleets++
latestTime = arrivalTime
}
}
return fleets
}
The Go implementation follows the same logic as the Python version. A custom Car struct stores each car's position and speed together.
The cars are sorted using sort.Slice. Since Go performs integer division by default, the code explicitly converts values to float64 before computing arrival times.
No special handling for nil slices is required because the problem guarantees valid input arrays of equal length.
Worked Examples
Example 1
Input:
target = 12
position = [10,8,0,5,3]
speed = [2,4,1,1,3]
First, pair and sort the cars:
| Position | Speed |
|---|---|
| 0 | 1 |
| 3 | 3 |
| 5 | 1 |
| 8 | 4 |
| 10 | 2 |
Now process from right to left.
| Car | Arrival Time | Latest Fleet Time | Action | Fleets |
|---|---|---|---|---|
| (10,2) | 1 | 0 | New fleet | 1 |
| (8,4) | 1 | 1 | Merge | 1 |
| (5,1) | 7 | 1 | New fleet | 2 |
| (3,3) | 3 | 7 | Merge | 2 |
| (0,1) | 12 | 7 | New fleet | 3 |
Final answer: 3
Example 2
Input:
target = 10
position = [3]
speed = [3]
| Car | Arrival Time | Latest Fleet Time | Action | Fleets |
|---|---|---|---|---|
| (3,3) | 7/3 | 0 | New fleet | 1 |
Final answer: 1
Example 3
Input:
target = 100
position = [0,2,4]
speed = [4,2,1]
Sorted cars:
| Position | Speed |
|---|---|
| 0 | 4 |
| 2 | 2 |
| 4 | 1 |
Process backward:
| Car | Arrival Time | Latest Fleet Time | Action | Fleets |
|---|---|---|---|---|
| (4,1) | 96 | 0 | New fleet | 1 |
| (2,2) | 49 | 96 | Merge | 1 |
| (0,4) | 25 | 96 | Merge | 1 |
Final answer: 1
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n log n) | Sorting dominates the runtime |
| Space | O(n) | Storing paired car data requires extra space |
The algorithm spends most of its time sorting the cars by position. The subsequent reverse traversal is linear. Since sorting n elements requires O(n log n) time, this becomes the overall complexity.
The auxiliary space comes from storing paired (position, speed) data. Aside from that, only a few scalar variables are used.
Test Cases
from typing import List
class Solution:
def carFleet(self, target: int, position: List[int], speed: List[int]) -> int:
cars = sorted(zip(position, speed))
fleets = 0
latest_time = 0.0
for pos, spd in reversed(cars):
arrival_time = (target - pos) / spd
if arrival_time > latest_time:
fleets += 1
latest_time = arrival_time
return fleets
sol = Solution()
assert sol.carFleet(12, [10,8,0,5,3], [2,4,1,1,3]) == 3
# Standard example with multiple merges
assert sol.carFleet(10, [3], [3]) == 1
# Single car case
assert sol.carFleet(100, [0,2,4], [4,2,1]) == 1
# All cars merge into one fleet
assert sol.carFleet(10, [0,4,8], [2,2,2]) == 3
# Equal speeds, no car can catch another
assert sol.carFleet(10, [6,8], [3,2]) == 2
# Faster rear car still cannot catch before target
assert sol.carFleet(10, [6,8], [4,2]) == 1
# Rear car catches exactly at target
assert sol.carFleet(20, [5,10,15], [1,1,1]) == 3
# Same speeds with different positions
assert sol.carFleet(20, [5,10,15], [3,2,1]) == 1
# Cascading merge into one fleet
assert sol.carFleet(15, [1,2,3,4,5], [5,4,3,2,1]) == 1
# Increasing slowdown causes all cars to merge
assert sol.carFleet(50, [10,20,30,40], [1,2,3,4]) == 4
# Faster cars are already ahead, so no merges
| Test | Why |
|---|---|
| Standard multi-fleet example | Verifies normal fleet merging behavior |
| Single car | Smallest valid input |
| All cars merge | Ensures cascading merges work |
| Equal speeds | Confirms no catching occurs |
| Cannot catch before target | Validates strict comparison logic |
| Catch exactly at target | Confirms target merge rule |
| Same speed sequence | Tests independent fleets |
| Descending fleet speeds | Tests chain merging |
| Progressive slowdown | Validates repeated merging |
| Faster cars already ahead | Ensures position ordering matters |
Edge Cases
One important edge case occurs when a car catches another exactly at the destination. A naive implementation might require the rear car to arrive strictly earlier in order to merge. However, the problem explicitly states that meeting at the target still forms one fleet. The implementation handles this correctly because cars merge whenever their arrival time is less than or equal to the fleet ahead's time.
Another subtle case involves equal speeds. If two cars travel at the same speed and start at different positions, the rear car can never catch the front car. A buggy solution might incorrectly merge them if it only compares speeds. This implementation avoids that issue entirely by comparing arrival times instead of raw speeds.
A third important case is cascading merges. A car may merge into a fleet that was itself formed by earlier merges. For example, car A merges into car B, and then car C merges into the resulting fleet. Simulating this incorrectly can be complicated, but the monotonic arrival-time approach naturally handles it. Once a fleet forms, its effective arrival time becomes fixed, and all later cars compare against that fleet time.
Another edge case is already sorted or reverse-sorted inputs. Since the algorithm explicitly sorts cars before processing, it does not rely on any initial ordering and remains correct for all valid inputs.