Files
codetutor/backend/data/questions/car-fleet.yaml
2025-05-25 10:16:13 +01:00

211 lines
11 KiB
YAML

title: Car Fleet
slug: car-fleet
difficulty: medium
leetcode_id: 853
leetcode_url: https://leetcode.com/problems/car-fleet/
categories:
- arrays
- sorting
- stack
patterns:
- monotonic-stack
description: |
There are `n` cars at given miles away from the starting mile `0`, travelling to reach the mile `target`.
You are given two integer arrays `position` and `speed`, both of length `n`, where `position[i]` is the starting mile of the i<sup>th</sup> car and `speed[i]` is the speed of the i<sup>th</sup> car in miles per hour.
A car cannot pass another car, but it can catch up and then travel next to it at the speed of the slower car.
A **car fleet** is a single car or a group of cars driving next to each other. The speed of the car fleet is the **minimum** speed of any car in the fleet.
If a car catches up to a car fleet at the mile `target`, it will still be considered as part of the car fleet.
Return *the number of car fleets that will arrive at the destination*.
constraints: |
- `n == position.length == speed.length`
- `1 <= n <= 10^5`
- `0 < target <= 10^6`
- `0 <= position[i] < target`
- All the values of `position` are **unique**
- `0 < speed[i] <= 10^6`
examples:
- input: "target = 12, position = [10,8,0,5,3], speed = [2,4,1,1,3]"
output: "3"
explanation: "The cars starting at 10 (speed 2) and 8 (speed 4) become a fleet, meeting each other at 12. The car starting at 0 (speed 1) does not catch up to any other car, so it is a fleet by itself. The cars starting at 5 (speed 1) and 3 (speed 3) become a fleet, meeting each other at 6."
- input: "target = 10, position = [3], speed = [3]"
output: "1"
explanation: "There is only one car, hence there is only one fleet."
- input: "target = 100, position = [0,2,4], speed = [4,2,1]"
output: "1"
explanation: "The cars starting at 0 (speed 4) and 2 (speed 2) become a fleet at mile 4. Then they catch up to the car at position 4 (speed 1), forming one fleet that reaches the target."
explanation:
intuition: |
Imagine watching a highway from above where all cars are heading towards the same destination. **A faster car behind a slower car will eventually catch up, but cannot pass** — they merge into a single fleet travelling at the slower speed.
The key insight is to think about **time to reach the target**. If we calculate how long each car takes to reach the destination (ignoring collisions), we can determine which cars will merge:
- A car that would arrive *faster* than the car directly ahead will catch up and merge with it
- A car that would arrive *slower* than the car ahead will never be caught — it leads its own fleet
Think of it like this: sort the cars by position from **closest to the target** to **farthest**. Process them in this order. For each car, calculate its arrival time. If this car would arrive later than or at the same time as the car ahead (the one closer to target), it forms or joins a fleet. If it would arrive sooner, it gets blocked by the slower fleet ahead.
By processing from front to back (closest to farthest from target), we can use a **monotonic stack** to track distinct fleets: each entry on the stack represents a fleet with a specific arrival time. A new car either merges with the fleet at the stack top (if it arrives faster) or starts a new fleet (if it arrives slower).
approach: |
We solve this using a **Sort + Monotonic Stack** approach:
**Step 1: Calculate time to reach target for each car**
- For each car, compute: `time = (target - position) / speed`
- This represents how long each car would take to reach the target if it could drive unimpeded
&nbsp;
**Step 2: Sort cars by position in descending order**
- Pair each car's position with its time-to-target
- Sort by position descending (closest to target first)
- This lets us process cars in the order they would encounter each other
&nbsp;
**Step 3: Use a stack to track fleet arrival times**
- Iterate through sorted cars
- For each car, compare its arrival time with the stack top (the fleet ahead):
- If the current car's time is **greater** than the top, it cannot catch up — push its time onto the stack (new fleet)
- If the current car's time is **less than or equal to** the top, it catches up and merges — do not push (absorbed into existing fleet)
&nbsp;
**Step 4: Return the stack size**
- Each entry in the stack represents a distinct fleet
- The number of entries is the answer
common_pitfalls:
- title: Not Sorting by Position
description: |
Processing cars in their given order doesn't work because car interactions depend on **relative positions**. A car at position 3 cannot affect a car at position 10 directly — only cars behind can catch up to cars ahead.
You must sort by position (descending) to process cars from closest-to-target to farthest. This ensures when we consider a car, all cars ahead of it have already been processed.
wrong_approach: "Processing cars in input order"
correct_approach: "Sort by position descending before processing"
- title: Using Speed Instead of Time
description: |
Comparing speeds directly is misleading. A car with higher speed doesn't always catch up — it depends on how far behind it starts.
For example, a car at position 8 with speed 4 and a car at position 0 with speed 100: despite the huge speed difference, the car at position 8 reaches target 12 in `(12-8)/4 = 1` hour, while the car at position 0 takes `12/100 = 0.12` hours. The faster car is too far behind to catch up before the target.
Always convert to **time-to-target** for accurate comparisons.
wrong_approach: "Comparing speeds to determine merging"
correct_approach: "Compare time-to-target values"
- title: Simulating Movement Step by Step
description: |
Trying to simulate the actual movement of cars over time is unnecessarily complex and slow. You don't need to track where cars are at each moment.
The mathematical insight is that arrival time tells us everything: if car A would arrive before car B (which is ahead), car A will catch up. We don't need to simulate *when* or *where* they merge.
wrong_approach: "Time-step simulation of car positions"
correct_approach: "Calculate arrival times and compare directly"
- title: Off-by-One in Fleet Counting
description: |
When a car merges with a fleet, don't double-count. The merged fleet still counts as **one** fleet, not two. Only increment the count (or push to stack) when a car forms a **new** fleet that cannot catch up to the one ahead.
key_takeaways:
- "**Transform the problem**: Converting positions and speeds into arrival times simplifies the merging logic dramatically"
- "**Monotonic stack pattern**: When processing elements in sorted order and tracking 'dominant' values (like slowest arrival times), a monotonic stack efficiently maintains the answer"
- "**Sort to establish order**: Many problems involving relative relationships (like cars on a road) require sorting to process elements in a meaningful sequence"
- "**Similar problems**: This pattern applies to collision problems, merge intervals based on relative metrics, and scheduling with dependencies"
time_complexity: "O(n log n). Sorting dominates the complexity. The stack operations are O(n) total since each car is pushed and popped at most once."
space_complexity: "O(n). We store the paired (position, time) data and the stack, each of which can hold up to `n` elements."
solutions:
- approach_name: Sort + Monotonic Stack
is_optimal: true
code: |
def car_fleet(target: int, position: list[int], speed: list[int]) -> int:
# Pair position with time-to-target, sort by position descending
cars = sorted(zip(position, speed), reverse=True)
stack = [] # Stack of arrival times for each fleet
for pos, spd in cars:
# Calculate time to reach target for this car
time = (target - pos) / spd
# If this car takes longer than the fleet ahead, it forms a new fleet
if not stack or time > stack[-1]:
stack.append(time)
# Otherwise, it catches up and merges (don't push)
# Each stack entry is a distinct fleet
return len(stack)
explanation: |
**Time Complexity:** O(n log n) — Sorting the cars by position.
**Space Complexity:** O(n) — Storage for sorted pairs and the stack.
We sort cars by position (closest to target first), then iterate through them. For each car, we calculate its arrival time. If it would arrive after the car ahead (top of stack), it can't catch up and forms a new fleet. Otherwise, it merges with the existing fleet. The stack size at the end equals the number of fleets.
- approach_name: Sort + Simple Counter
is_optimal: true
code: |
def car_fleet(target: int, position: list[int], speed: list[int]) -> int:
# Pair position with time-to-target, sort by position descending
cars = sorted(zip(position, speed), reverse=True)
fleets = 0
slowest_time = 0 # Time of the slowest fleet we've seen
for pos, spd in cars:
time = (target - pos) / spd
# If this car is slower than all fleets ahead, it leads a new fleet
if time > slowest_time:
fleets += 1
slowest_time = time
# Otherwise, it gets absorbed by a slower fleet ahead
return fleets
explanation: |
**Time Complexity:** O(n log n) — Sorting the cars by position.
**Space Complexity:** O(1) — Only tracking two variables (excluding input/sort space).
This is a space-optimised variant. Since we only need to track the slowest arrival time seen so far (not the full stack), we can use a single variable. A car forms a new fleet only if it would arrive later than all fleets ahead of it.
- approach_name: Brute Force Simulation
is_optimal: false
code: |
def car_fleet(target: int, position: list[int], speed: list[int]) -> int:
n = len(position)
# Calculate time to reach target for each car
times = [(target - position[i]) / speed[i] for i in range(n)]
# Sort indices by position descending
indices = sorted(range(n), key=lambda i: position[i], reverse=True)
fleets = 0
max_time = 0
for i in indices:
if times[i] > max_time:
fleets += 1
max_time = times[i]
return fleets
explanation: |
**Time Complexity:** O(n log n) — Sorting by position.
**Space Complexity:** O(n) — Storing times and sorted indices.
This approach sorts indices rather than creating pairs, which some find more readable. It's functionally equivalent to the simple counter solution but uses more space by keeping the original arrays separate.