1870. Minimum Speed to Arrive on Time
Problem Description
You need to commute to the office by taking n
trains in sequential order. You have a total of hour
hours to complete your journey.
Given:
- A floating-point number
hour
: the total time available to reach the office - An integer array
dist
of lengthn
: wheredist[i]
represents the distance (in kilometers) of thei-th
train ride
Key constraints:
- Each train can only depart at an integer hour mark
- You must wait between train rides if needed
- For example, if the first train ride takes 1.5 hours at a certain speed, you must wait an additional 0.5 hours before boarding the second train at the 2-hour mark
The goal is to find the minimum positive integer speed (in kilometers per hour) that all trains must travel at for you to reach the office within the given time limit. If it's impossible to arrive on time, return -1
.
The waiting time mechanism works as follows:
- For all trains except the last one, if the travel time is not a whole number, you must wait until the next integer hour to board the next train
- For the last train, no waiting is needed since you've reached your destination
Example of time calculation:
- If you have 3 trains with distances
[1, 3, 2]
km and speed is 2 km/h:- First train: takes 0.5 hours, wait 0.5 hours → depart second train at hour 1
- Second train: takes 1.5 hours, wait 0.5 hours → depart third train at hour 3
- Third train: takes 1 hour → arrive at hour 4
- Total time: 4 hours
Additional constraints:
- The answer will not exceed
10^7
hour
will have at most two decimal places
Intuition
The key observation is that speed and time have an inverse relationship - as speed increases, the total time to complete the journey decreases. This monotonic property is crucial for our solution.
Consider what happens at different speeds:
- At a very low speed (say 1 km/h), trains take a long time to cover their distances, resulting in a large total time
- At a higher speed, the same distances are covered faster, reducing the total time
- Once we find a speed that allows us to arrive within
hour
hours, any higher speed will also work
This monotonic behavior suggests binary search as an efficient approach. Instead of checking every possible speed from 1 to some maximum value, we can use binary search to quickly narrow down to the minimum speed that works.
Before diving into the search, we need to establish if a solution is even possible. Each train (except possibly the last) requires at least one full hour due to the integer departure constraint. If we have n
trains and only hour
hours available, where n > ceil(hour)
, it's mathematically impossible to complete the journey. For instance, with 5 trains and only 4.5 hours, even at infinite speed, we'd need at least 4 hours for the first 4 trains (due to waiting times) plus some time for the last train.
For any given speed v
, we can calculate the total time needed:
- For each train except the last: time =
ceil(dist[i] / v)
(accounting for waiting time) - For the last train: time =
dist[n-1] / v
(no waiting needed)
The search space for speed is bounded:
- Lower bound: 1 km/h (minimum positive integer speed)
- Upper bound:
10^7
km/h (given constraint)
We binary search within this range to find the smallest speed where the total calculated time is less than or equal to hour
.
Learn more about Binary Search patterns.
Solution Approach
The implementation uses binary search to find the minimum speed required. Let's walk through the key components:
Initial Feasibility Check:
First, we check if it's possible to reach on time. If the number of trains len(dist)
is greater than ceil(hour)
, we immediately return -1
. This is because even at infinite speed, we'd need at least n-1
full hours for the first n-1
trains due to the waiting constraint.
Binary Search Setup:
- Left boundary
l = 1
: minimum possible speed (1 km/h) - Right boundary
r = 10^7 + 1
: maximum possible speed plus one - We use
bisect_left
to find the leftmost position where our condition becomes true
Speed Validation Function:
The check(v)
function determines if a given speed v
allows us to arrive within the time limit:
def check(v: int) -> bool:
s = 0
for i, d in enumerate(dist):
t = d / v
s += t if i == len(dist) - 1 else ceil(t)
return s <= hour
For each train ride:
- Calculate travel time:
t = d / v
- If it's the last train (
i == len(dist) - 1
), add the exact timet
- Otherwise, add
ceil(t)
to account for waiting until the next integer hour - Return
True
if total times
is within the limit
Binary Search Execution:
The solution uses Python's bisect_left
with a custom key function:
ans = bisect_left(range(1, r), True, key=check) + 1
This finds the smallest speed in the range [1, r)
where check(speed)
returns True
. The +1
adjusts for the 0-based indexing of the range.
Result Handling:
- If
ans == r
(equals10^7 + 1
), no valid speed exists within our constraints, return-1
- Otherwise, return
ans
as the minimum required speed
The binary search efficiently narrows down from a potential range of 10^7
values to find the exact minimum speed in O(log(10^7))
iterations, with each iteration taking O(n)
time to validate the speed.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a concrete example to illustrate the solution approach.
Example: dist = [1, 3, 2]
, hour = 2.5
Step 1: Initial Feasibility Check
- Number of trains:
n = 3
ceil(2.5) = 3
- Since
3 ≤ 3
, it's potentially possible to complete the journey
Step 2: Binary Search Setup
- Left boundary:
l = 1
km/h - Right boundary:
r = 10^7 + 1
km/h - We'll search for the minimum speed where we can complete the journey in ≤ 2.5 hours
Step 3: Binary Search Process
Let's trace through a few iterations:
First iteration: mid = (1 + 10^7) / 2 ≈ 5,000,000
km/h
- Train 1:
1/5,000,000
hours ≈ 0, wait until hour 1 - Train 2:
3/5,000,000
hours ≈ 0, wait until hour 2 - Train 3:
2/5,000,000
hours ≈ 0 - Total time ≈ 2 hours < 2.5 ✓
- This speed works, search lower half
Several iterations later: mid = 3
km/h
- Train 1:
1/3 = 0.33
hours →ceil(0.33) = 1
hour (wait until hour 1) - Train 2:
3/3 = 1
hour →ceil(1) = 1
hour (depart at hour 2) - Train 3:
2/3 = 0.67
hours (no waiting for last train) - Total time =
1 + 1 + 0.67 = 2.67
hours > 2.5 ✗ - This speed doesn't work, search higher speeds
Next iteration: mid = 4
km/h
- Train 1:
1/4 = 0.25
hours →ceil(0.25) = 1
hour - Train 2:
3/4 = 0.75
hours →ceil(0.75) = 1
hour - Train 3:
2/4 = 0.5
hours - Total time =
1 + 1 + 0.5 = 2.5
hours = 2.5 ✓ - This speed works exactly!
Continue searching: Check if speed 3 works (it doesn't, as shown above)
Step 4: Result The binary search converges to find that the minimum speed is 4 km/h.
At this speed:
- We board train 1 at hour 0, arrive at 0.25, wait until hour 1
- We board train 2 at hour 1, arrive at 1.75, wait until hour 2
- We board train 3 at hour 2, arrive at 2.5
- Total journey time: exactly 2.5 hours
This demonstrates how the binary search efficiently finds the minimum speed by testing different values and narrowing the search range based on whether each speed allows us to complete the journey on time.
Solution Implementation
1from typing import List
2from math import ceil
3from bisect import bisect_left
4
5class Solution:
6 def minSpeedOnTime(self, dist: List[int], hour: float) -> int:
7 """
8 Find the minimum speed needed to reach the destination within the given time.
9
10 Args:
11 dist: List of distances for each train segment
12 hour: Maximum time allowed to complete the journey
13
14 Returns:
15 Minimum speed required, or -1 if impossible
16 """
17
18 def can_reach_in_time(speed: int) -> bool:
19 """
20 Check if we can complete the journey within the time limit at given speed.
21
22 For all trains except the last, we must wait for the next integer hour.
23 For the last train, we can arrive at any fractional time.
24 """
25 total_time = 0
26
27 for index, distance in enumerate(dist):
28 travel_time = distance / speed
29
30 # For the last train, we don't need to wait for the next hour
31 if index == len(dist) - 1:
32 total_time += travel_time
33 else:
34 # For other trains, round up to next integer hour
35 total_time += ceil(travel_time)
36
37 return total_time <= hour
38
39 # If we have more trains than available hours (rounded up), it's impossible
40 # Since we need at least 1 hour per train (except possibly the last one)
41 if len(dist) > ceil(hour):
42 return -1
43
44 # Maximum possible speed to search (10^7 based on problem constraints)
45 max_speed = 10**7 + 1
46
47 # Binary search for the minimum speed that satisfies the time constraint
48 # bisect_left finds the leftmost position where True would be inserted
49 min_speed = bisect_left(range(1, max_speed), True, key=can_reach_in_time) + 1
50
51 # If no valid speed found within the search range
52 if min_speed == max_speed:
53 return -1
54
55 return min_speed
56
1class Solution {
2 /**
3 * Finds the minimum speed required to reach the destination within the given time limit.
4 * Uses binary search to find the optimal speed.
5 *
6 * @param dist Array of distances for each segment of the journey
7 * @param hour Maximum time allowed to complete the journey
8 * @return Minimum speed required, or -1 if impossible
9 */
10 public int minSpeedOnTime(int[] dist, double hour) {
11 // Check if it's impossible to complete the journey
12 // We need at least (n-1) full hours plus some time for the last segment
13 if (dist.length > Math.ceil(hour)) {
14 return -1;
15 }
16
17 // Define search boundaries
18 final int MAX_SPEED = (int) 1e7;
19 int left = 1; // Minimum possible speed
20 int right = MAX_SPEED + 1; // Maximum possible speed + 1 (exclusive)
21
22 // Binary search for the minimum valid speed
23 while (left < right) {
24 int mid = (left + right) >> 1; // Equivalent to (left + right) / 2
25
26 if (canReachOnTime(dist, mid, hour)) {
27 // If we can reach with this speed, try a lower speed
28 right = mid;
29 } else {
30 // If we cannot reach with this speed, we need a higher speed
31 left = mid + 1;
32 }
33 }
34
35 // Return the result, or -1 if no valid speed exists
36 return left > MAX_SPEED ? -1 : left;
37 }
38
39 /**
40 * Checks if it's possible to complete the journey within the time limit at given speed.
41 *
42 * @param dist Array of distances for each segment
43 * @param speed Speed to test
44 * @param hour Maximum time allowed
45 * @return true if journey can be completed within time limit, false otherwise
46 */
47 private boolean canReachOnTime(int[] dist, int speed, double hour) {
48 double totalTime = 0;
49 int numSegments = dist.length;
50
51 // Calculate total time for the journey
52 for (int i = 0; i < numSegments; ++i) {
53 // Calculate time for current segment
54 double timeForSegment = (double) dist[i] / speed;
55
56 // For all segments except the last, we must wait for the next whole hour
57 // For the last segment, we can arrive at any fractional hour
58 if (i == numSegments - 1) {
59 totalTime += timeForSegment;
60 } else {
61 totalTime += Math.ceil(timeForSegment);
62 }
63 }
64
65 // Check if total time is within the limit
66 return totalTime <= hour;
67 }
68}
69
1class Solution {
2public:
3 int minSpeedOnTime(vector<int>& dist, double hour) {
4 // If number of trains is greater than ceiling of hour, impossible to arrive on time
5 // (Each train except last takes at least 1 hour, last train takes > 0 time)
6 if (dist.size() > ceil(hour)) {
7 return -1;
8 }
9
10 // Set search range for binary search
11 const int MAX_SPEED = 1e7;
12 int left = 1; // Minimum possible speed
13 int right = MAX_SPEED + 1; // Maximum possible speed + 1 (exclusive)
14 int numTrains = dist.size();
15
16 // Lambda function to check if given speed allows arrival within time limit
17 auto canArriveOnTime = [&](int speed) {
18 double totalTime = 0;
19
20 // Calculate total travel time
21 for (int i = 0; i < numTrains; ++i) {
22 double travelTime = static_cast<double>(dist[i]) / speed;
23
24 // For all trains except the last, must wait for next integer hour
25 // For the last train, can arrive at any fractional time
26 if (i == numTrains - 1) {
27 totalTime += travelTime;
28 } else {
29 totalTime += ceil(travelTime);
30 }
31 }
32
33 return totalTime <= hour;
34 };
35
36 // Binary search for minimum speed
37 while (left < right) {
38 int mid = left + (right - left) / 2; // Avoid potential overflow
39
40 if (canArriveOnTime(mid)) {
41 // If current speed works, try to find a smaller speed
42 right = mid;
43 } else {
44 // If current speed doesn't work, need higher speed
45 left = mid + 1;
46 }
47 }
48
49 // Check if the result exceeds maximum allowed speed
50 return left > MAX_SPEED ? -1 : left;
51 }
52};
53
1/**
2 * Finds the minimum speed needed to arrive on time given distances and time limit
3 * @param dist - Array of distances for each train segment
4 * @param hour - Maximum time allowed for the entire journey
5 * @returns Minimum speed required, or -1 if impossible
6 */
7function minSpeedOnTime(dist: number[], hour: number): number {
8 // If we have more trains than available hours (rounded up), it's impossible
9 // Each train except the last takes at least 1 hour (we must wait for the next hour)
10 if (dist.length > Math.ceil(hour)) {
11 return -1;
12 }
13
14 const trainCount: number = dist.length;
15 const maxSpeed: number = 10 ** 7; // Maximum possible speed constraint
16
17 /**
18 * Checks if a given speed allows completing the journey within time limit
19 * @param speed - The speed to test
20 * @returns true if journey can be completed within hour limit at this speed
21 */
22 const canArriveOnTime = (speed: number): boolean => {
23 let totalTime: number = 0;
24
25 for (let i = 0; i < trainCount; ++i) {
26 const timeForSegment: number = dist[i] / speed;
27
28 // For all trains except the last, we must wait until the next hour
29 // For the last train, we can arrive at any fractional hour
30 if (i === trainCount - 1) {
31 totalTime += timeForSegment;
32 } else {
33 totalTime += Math.ceil(timeForSegment);
34 }
35 }
36
37 return totalTime <= hour;
38 };
39
40 // Binary search for the minimum speed
41 let leftBound: number = 1;
42 let rightBound: number = maxSpeed + 1;
43
44 while (leftBound < rightBound) {
45 const midSpeed: number = (leftBound + rightBound) >> 1; // Equivalent to Math.floor((left + right) / 2)
46
47 if (canArriveOnTime(midSpeed)) {
48 // If we can arrive on time at this speed, try slower speeds
49 rightBound = midSpeed;
50 } else {
51 // If we cannot arrive on time, we need faster speeds
52 leftBound = midSpeed + 1;
53 }
54 }
55
56 // If the minimum speed exceeds the maximum allowed, return -1
57 return leftBound > maxSpeed ? -1 : leftBound;
58}
59
Time and Space Complexity
The time complexity is O(n × log M)
, where n
is the length of the dist
array (number of train trips) and M
is the upper bound of the speed (10^7 + 1
in this case).
This complexity arises from:
- The binary search performed by
bisect_left
over the range[1, r)
wherer = 10^7 + 1
, which takesO(log M)
iterations - For each iteration of the binary search, the
check
function is called, which iterates through alln
elements in thedist
array, takingO(n)
time - Therefore, the total time complexity is
O(n × log M)
The space complexity is O(1)
as the algorithm only uses a constant amount of extra space for variables like s
, t
, ans
, and r
, regardless of the input size. The check
function and binary search operation do not require any additional space that scales with the input.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Handling of the Last Train's Travel Time
Pitfall: A common mistake is applying the ceiling function to ALL train travel times, including the last one. This would incorrectly round up the last train's travel time to the next integer hour.
# INCORRECT implementation
def check(v: int) -> bool:
s = 0
for d in dist:
s += ceil(d / v) # Wrong: applies ceiling to all trains
return s <= hour
Why it's wrong: The last train doesn't require waiting since you've reached your destination. If the last train takes 0.5 hours, the total time should include exactly 0.5 hours, not 1 hour.
Solution: Always check if you're on the last train and handle it differently:
def check(v: int) -> bool:
s = 0
for i, d in enumerate(dist):
t = d / v
s += t if i == len(dist) - 1 else ceil(t)
return s <= hour
2. Integer Division Instead of Float Division
Pitfall: Using integer division (//
) when calculating travel time, which loses precision:
# INCORRECT - loses decimal precision travel_time = distance // speed # Wrong: integer division
Why it's wrong: If distance is 3 and speed is 2, integer division gives 1 instead of 1.5. This would underestimate the actual travel time.
Solution: Always use float division (/
) to maintain precision:
travel_time = distance / speed # Correct: float division
3. Off-by-One Error in Binary Search Boundaries
Pitfall: Setting incorrect boundaries or forgetting to adjust the result from bisect_left
:
# INCORRECT - forgets to add 1
ans = bisect_left(range(1, max_speed), True, key=check) # Missing +1
Why it's wrong: bisect_left
returns an index into the range starting from 0, but our speed range starts from 1. Without adding 1, you'd return speed 0 for the first valid position.
Solution: Remember to add 1 to adjust for the starting position:
ans = bisect_left(range(1, max_speed), True, key=check) + 1
4. Not Checking Early Impossibility Cases
Pitfall: Performing binary search without first checking if the problem is solvable:
# INCORRECT - missing feasibility check
def minSpeedOnTime(self, dist: List[int], hour: float) -> int:
# Directly starts binary search without checking if solution exists
return bisect_left(range(1, 10**7 + 1), True, key=check) + 1
Why it's wrong: If you have 5 trains but only 3.5 hours, it's impossible to complete the journey regardless of speed, since you need at least 4 hours (waiting for integer hours for the first 4 trains).
Solution: Check if len(dist) > ceil(hour)
before starting the search:
if len(dist) > ceil(hour):
return -1
Which two pointer techniques do you use to check if a string is a palindrome?
Recommended Readings
Binary Search Speedrun For each of the Speedrun questions you will be given a binary search related problem and a corresponding multiple choice question The multiple choice questions are related to the techniques and template s introduced in the binary search section It's recommended that you have gone through at
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!