Problem overview
Cars drive on a one-lane road to a destination target miles from the start. Car i starts at mile position[i] and drives at speed[i] miles per hour. A car can't pass the car ahead: when it catches up, it slows down and the two drive on together at the front car's speed. A group of cars driving together is a fleet, and a car on its own is a fleet too. A car that catches up exactly at target still belongs to the fleet it reached. The method returns how many fleets arrive.
In the first example, target = 12, position = [10, 8, 0, 5, 3] and speed = [2, 4, 1, 1, 3], the answer is 3. The cars from miles 10 and 8 would both arrive after 1 hour, so they form one fleet. The cars from miles 5 and 3 meet and drive on at speed 1, arriving after 7 hours: a second fleet. The car from mile 0 never catches anyone: a third.
Approach
Simulating the road minute by minute, or checking every pair of cars, is slow and awkward. The key idea is to look only at the time each car would need if nothing slowed it down: (target - position[i]) / speed[i] hours.
Look at the cars from the one closest to the target to the farthest. The closest car can never be blocked. Each next car can only meet the cars ahead of it, and those are already handled. The fleet just ahead of the current car is always the last one found so far, and it is also the one that arrives last. So compare the car with that fleet only:
- If the car would arrive no later than that fleet, it catches up with it at the latest at the target and joins it. Nothing to do.
- If the car would arrive later, it can't catch up. It leads a new fleet, and the cars behind it may still join this new one.
A fleet arrives when the car that leads it would arrive on its own, since that car was never slowed down, so the stack only needs the number of the lead car of each fleet. The answer is the size of the stack. Sorting the cars by position costs O(n log n), and the pass after it is O(n).
The times are fractions, so the lesson compares them without dividing, as explained below.
Step by step
- Sort the car numbers.
val order = position.indices.sortedByDescending { position[it] }.position.indicesis the range of the indexes, andsortedByDescendingreturns them as a new list, largest key first. The key is the car's position, so the car closest to the target comes first. Sorting car numbers rather than positions keeps each position tied to its speed. - The stack.
val fleets = ArrayDeque<Int>()holds, for each fleet, the number of its lead car. The last element is the top. - The loop.
for (car in order)visits the cars from front to back. - New fleet or not. Inside, an
ifchecksfleets.isEmpty() || arrivesLater(target, position, speed, car, fleets.last()). The||stops at the first true part, solast()is only read when the stack isn't empty. - Push. In that
if,fleets.addLast(car): the car leads a new fleet. - Answer. After the loop,
return fleets.size. - The helper.
arrivesLatertakestarget,position,speed,carandlead, and answers whethercar, driving alone, would arrive strictly later thanlead. Strictly, because a car that arrives at the same moment joins the fleet. - The comparison. It multiplies both sides of the time comparison by the two speeds, which are positive, and compares
(target - position[car]).toLong() * speed[lead]with(target - position[lead]).toLong() * speed[car].
The solution in Kotlin
class Solution {
fun carFleet(target: Int, position: IntArray, speed: IntArray): Int {
val order = position.indices.sortedByDescending { position[it] }
val fleets = ArrayDeque<Int>()
for (car in order) {
if (fleets.isEmpty() || arrivesLater(target, position, speed, car, fleets.last())) {
fleets.addLast(car)
}
}
return fleets.size
}
fun arrivesLater(target: Int, position: IntArray, speed: IntArray, car: Int, lead: Int): Boolean {
return (target - position[car]).toLong() * speed[lead] > (target - position[lead]).toLong() * speed[car]
}
}
Why compare without dividing, and why toLong()
The comparison (target - position[car]) / speed[car] > (target - position[lead]) / speed[lead] is the same as the cross-multiplied one, because both speeds are positive. Cross-multiplying keeps the times exact: equal times stay exactly equal, with no rounding.
The two ways around it both fail:
- Integer division. In Kotlin,
Int / Inttruncates. Taketarget = 13with a lead car at mile 10 and speed 2 (1.5 hours) and a car at mile 8 with speed 3 (1.6666666666666667 hours). The second would arrive later and starts its own fleet, so the answer is 2. But(13 - 8) / 3 == (13 - 10) / 2istrue, since both truncate to 1, and a solution using integer division merges the cars into one fleet. - Overflow. Each product can reach about a trillion (10^12), far beyond what an
Int(32 bits, about 2.1 billion) holds, andIntoverflow wraps silently. For example,(1_000_000 - 0) * 1_000_000inIntprints-727379968. Withtarget = 1_000_000,position = [0, 999_999]andspeed = [1, 1_000_000], the rear car needs 1,000,000 hours and the front car 0.000001 hours, so the rear car arrives later and the answer is 2. The cross-multiplied comparison inLongsays so (true), but the same comparison inIntsaysfalse.
.toLong() on the first factor makes the multiplication happen in 64 bits, because a Long times an Int is a Long. The subtraction inside the parentheses is done on Ints first, and that is safe: it is at most a million.
A trace
For the first example, order is [0, 1, 3, 4, 2]: the cars from miles 10, 8, 5, 3 and 0. Their times alone are 1.0, 1.0, 7.0, 3.0 and 12.0 hours.
- The car from mile 10 starts the stack: one fleet, lead time 1.0.
- The car from mile 8 also takes 1.0, which is not strictly later, so it joins.
- The car from mile 5 takes 7.0, later than 1.0: a new fleet.
- The car from mile 3 takes 3.0, not later than 7.0: it joins the fleet of mile 5.
- The car from mile 0 takes 12.0, later than 7.0: a third fleet.
The method returns 3.
Complexity
Time: O(n log n). Sorting costs O(n log n). The loop does a constant amount of work per car, so sorting is the larger part.
Space: O(n). The sorted list of car numbers and the stack each hold at most n entries.
Edge cases and common mistakes
- Sorting the positions only. If you sort
positionon its own, the speeds no longer match. Sort the car numbers by position instead. - Using the wrong direction. The loop must start with the car closest to the target. Going from the back would compare each car with a car it has not yet had to deal with.
- Using >= in the helper. A car that arrives at exactly the same time as the fleet ahead belongs to it (the first two cars of the first example). The check must be strict.
- Cars with equal speed. They never meet unless they share a position, and positions are all different. For
target = 10,position = [0, 5]andspeed = [1, 1]the answer is 2. - A single car is one fleet:
carFleet(10, intArrayOf(3), intArrayOf(3))returns 1. - Dividing as
Intor overflowing, as shown above.
Testing
These calls were run against the solution above:
target | position | speed | Result |
|---|---|---|---|
| 12 | [10, 8, 0, 5, 3] | [2, 4, 1, 1, 3] | 3 |
| 10 | [3] | [3] | 1 |
| 100 | [0, 2, 4] | [4, 2, 1] | 1 |
| 10 | [0, 4, 2] | [2, 1, 3] | 1 |
| 10 | [6, 8] | [3, 2] | 2 |
| 10 | [0, 5] | [1, 1] | 2 |
| 13 | [10, 8] | [2, 3] | 2 |
| 1000000 | [0, 999999] | [1, 1000000] | 2 |
