DSA sheet · Binary Search · Binary search on answer
Minimum Speed to Arrive on Time
The third problem of the binary search on answer pattern. It looks a lot like Koko Eating Bananas (find the slowest speed that's still fast enough), but with three twists the teacher focuses on: the time limit is a decimal number, the last train is not rounded up, and the upper limit of the search does not come from the array (it's 10⁷, straight from the problem statement). It can also be impossible, so we may have to return −1.
Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember
- Part 0 · What you must know before starting
- Part A · Brute force: try every speed from 1 to 10⁷
- Part B · Optimal: binary search on the speed
- Part C · Revision page
Part 0 · Before starting
Why binary search works at all
Binary search works whenever we have a yes/no question that is monotonic: as we move along the values, the answer is "no, no, no…" and then switches to "yes, yes, yes…" exactly once (or the reverse). Testing the middle value tells us which side of the middle the switch lies on, so we throw the other side away. A sorted array is one such case, but a range of numbers that we choose works just as well.
low, high, mid
low,high: the ends of the part still being searched.mid: the middle value we test.while low <= high: the teacher explains why<=and not<: when low and high point at the same single speed, that speed still has to be tested. With<it would be skipped.
Why mid = low + (high - low) // 2?
In Java/C++, (low + high) / 2 can overflow if low + high goes past the int limit (about 2.1 × 10⁹). low + (high - low) / 2 never does. Here high is 10⁷ so it wouldn't overflow anyway, but the safe form is a good habit. Python ints never overflow; both forms give the same result.
The pattern: binary search on answer
- Answer range: pick the smallest possible answer (
low) and the largest we'd ever need (high). - Check function:
check(speed)= "at this speed, do I reach the office in time?" - Monotonic: time = distance ÷ speed. Faster trains never take more time. So once a speed works, every faster speed works: no no no yes yes yes.
- Which side to keep: we want the minimum speed. Yes at mid → save it, go left. No → go right.
speeds: 1 ........................................ 10^7
on time?: no no no no YES YES YES YES YES
^
the first YES = minimum speed
(if even 10^7 says "no", there is no YES at all → return -1)
Floor and ceiling
Ceiling = round up to the next whole number (ceil(1.5) = 2, ceil(2.0) = 2). Floor = round down. In Python, math.ceil(x) gives the ceiling, and for whole numbers (a + b - 1) // b gives ceil(a / b) without any decimals.
Part A · Brute force: try every speed from 1 to 10⁷
LeetCode 1870
1The question in simple words
You must reach the office within hour hours. hour is a decimal number (for example 2.7). To get there you take n trains one after another, in the given order. Train i travels dist[i] km. All trains run at the same speed, which you choose (a positive whole number, km per hour).
The catch: a train can only leave at a whole hour (1:00, 2:00, …). If a ride takes 1.5 hours, you wait 0.5 hours for the next train, which leaves at hour 2. So every ride except the last one effectively costs a whole number of hours (rounded up). After the last ride there is no next train to wait for, so its time counts exactly.
Return the minimum speed that gets you there on time, or −1 if no speed can.
2What the constraints tell us
1 <= n <= 10⁵,1 <= dist[i] <= 10⁵.1 <= hour <= 10⁹, with at most two digits after the decimal point (like 2.70 or 1.09).- The answer will not exceed 10⁷. The teacher underlines this last line: it means 10⁷ itself can be the answer, and nothing above it ever is. This is where our high comes from.
3Intuition
Same as Koko: if you didn't know binary search, you'd try speed 1, then 2, then 3… and for each speed add up the travel time of all the trains. The first speed whose total time is within hour is the minimum, because we go upward. If none of them works, return −1.
4Building the logic from examples
Example 1: dist = [1, 3, 2], hour = 6
At speed 1: 1 h + 3 h + 2 h = 6 h, and no waiting is needed (each ride ends on a whole hour). 6 ≤ 6 → speed 1 works. Nothing is slower than 1, so 1 is the answer.
Example 2: dist = [1, 3, 2], hour = 2.7
At speed 3:
- Train 1: 1/3 = 0.33 h, but the next train leaves only at hour 1 → counts as 1 h.
- Train 2: 3/3 = 1 h → 1 h. Now we're at hour 2.
- Train 3 (last): 2/3 = 0.67 h → counted exactly, no waiting after it.
Total 2.67 h ≤ 2.7 → on time. (Speed 2 gives 1 + 2 + 1.0 = 4 h, too slow.) Answer 3.
→ Rounding up is the waiting for the next train, which can only leave at a whole hour. After the last train you are already at the office; there's nothing to wait for. So the last ride is added as an exact decimal. This is why, in code, we compute each ride's time first and only then decide: "is this the last index? add exactly; otherwise add the ceiling".
Choosing low and high
- low = 1. Speed must be a positive integer; 0 or negative makes no sense.
- high = 10⁷. Not max(dist), not sum(dist) as in earlier problems. The statement itself promises the answer is at most 10⁷, so that's the upper limit.
→ Because of the decimal last ride. Take dist = [1, 1, 100000], hour = 2.01. The first two rides take 1 hour each no matter how fast (we wait for the whole hour). That leaves only 0.01 hour for 100000 km, so we need speed 100000 / 0.01 = 10⁷, far above max(dist) = 10⁵. Since hour has at most 2 decimals, 0.01 h is the smallest slot, and 10⁵ / 0.01 = 10⁷ is the most ever needed. That's where the problem's 10⁷ comes from. The teacher's advice: when you code this on your own, remember why it's 10⁷ and not max or sum.
Example 3: dist = [1, 3, 2], hour = 1.9 → −1 (the base case)
Try the fastest possible speed, 10⁷. Train 1: 0.0000001 h, but we wait → 1 h. Train 2: again → 1 h. Train 3: 2/10⁷ ≈ 0.0000002 h. Total ≈ 2.0000002 h. Even at top speed it's more than 1.9 → impossible → −1.
The teacher generalises: the first n − 1 trains each cost at least 1 whole hour, and the last one costs a bit more than 0. So the total is always more than n − 1 hours. With n = 3 the total is at least "2.something"; since hour has two decimals, the smallest hour that can work is 2.01. With n = 4, it's 3.01.
if hour <= n - 1: return -1She says you can add it or skip it. Without it, the binary search simply never finds a "yes" and returns −1 anyway, and the big-O time is the same. (For the brute force it saves a lot: otherwise it would try all 10⁷ speeds before giving up.)
5Approach steps
- (Optional) if
hour <= n - 1→ return −1. - For each speed from 1 to 10⁷:
- time = 0.0. For each train j: t = dist[j] / speed. If j is not the last index, add ceil(t); else add t.
- If time ≤ hour → return this speed (first one = minimum).
- After the loop → return −1.
6Code (Python)
import math
class Solution:
def minSpeedOnTime(self, dist, hour):
n = len(dist)
if hour <= n - 1: # optional base case
return -1
for speed in range(1, 10**7 + 1):
time = 0.0
for j in range(n):
t = dist[j] / speed # ride time as a decimal
if j != n - 1:
time += math.ceil(t) # wait for the whole hour
else:
time += t # last ride: exact
if time <= hour:
return speed # first that works = minimum
return -17Code line by line
| line | what it means |
|---|---|
| if hour <= n - 1: return -1 | Even infinitely fast trains need more than n − 1 hours. |
| for speed in range(1, 10**7 + 1): | Every candidate speed, slowest first. |
| time = 0.0 | A decimal total (Java needs a double here; she also type-casts dist to double before dividing so it isn't integer division). |
| t = dist[j] / speed | Exact ride time. In Python / always gives a decimal. |
| if j != n - 1: time += math.ceil(t) | Not the last train: you wait until the next whole hour. |
| else: time += t | Last train: no waiting after it. |
| if time <= hour: return speed | On time. Because we go upward, the first one is the minimum: if 4 is the first to work, 5 and 6 work too, but 4 is the smallest. |
| return -1 | No speed in the whole range worked. |
8Dry run
dist = [1, 3, 2], hour = 2.7. Base case: 2.7 > 2, so continue.
| speed | train 1 | train 2 | train 3 (exact) | total | ≤ 2.7 ? |
|---|---|---|---|---|---|
| 1 | 1 | 3 | 2.0 | 6.0 | no |
| 2 | 0.5 → 1 | 1.5 → 2 | 1.0 | 4.0 | no |
| 3 | 0.33 → 1 | 1 | 0.67 | 2.67 | yes → return 3 |
speed : 1 2 3 4 5 ... 10^7 time : 6.0 4.0 2.67 2.5 2.4 ... 2.0000002 <= 2.7? : no no YES YES YES ... YES ^ first YES = 3
9Complexity & remember
- Outer loop: up to 10⁷ speeds (from the statement). Inner loop: n ≤ 10⁵ trains. Multiplied: 10¹² → TLE.
- The inner loop cannot be reduced: to know the total time we must look at every train. The outer loop is the one to fix.
- Space O(1).
Part B · Optimal: binary search on the speed
1The question
Same as Part A. The time calculation moves into a function, canReach(dist, hour, speed), and the outer loop becomes a binary search.
2Constraints
The range 1 … 10⁷ is sorted (it's a number line), and halving it takes only log₂(10⁷) ≈ 24 steps.
3Intuition
Why test speeds one after another when they're sorted and the yes/no answer flips only once? Test the middle speed and drop half. That's the whole change. This is also why the teacher asked us to put the time calculation in its own function: so we can call it with any mid.
4Building the decisions
- canReach(mid) is True: mid is on time, so it may be the answer. Save it in
ans. We want the minimum, so look left:high = mid - 1. - canReach(mid) is False: say we had 6 hours and mid took 8. To cut the time we can't change the distance (time = distance ÷ speed), so we must raise the speed:
low = mid + 1. - ans starts at −1. If no speed ever works, it's never overwritten, and −1 is exactly what the question wants for "impossible".
time <= hour always safe?→ Almost always, but not at the exact boundary. Computers store decimals like 0.14 only approximately. Take
dist = [50, 7], hour = 1.14. At speed 50 the true time is 1 + 7/50 = exactly 1.14, so 50 should work. But in Python, 1 + 7/50 gives 1.1400000000000001, which is "greater" than 1.14, so the float version rejects 50 and returns 51 instead of 50.Fix: use only whole numbers. hour has at most 2 decimals, so
H = round(hour * 100) is hour in hundredths. Let full = the whole hours of the first n − 1 rides. Then "full + last/speed ≤ H/100" is the same as 100 * (full * speed + last) <= H * speed (multiply both sides by 100 × speed, which is positive). No decimals, no rounding errors. The safe version is below; the logic is identical.5Approach steps
- (Optional) if
hour <= n - 1→ −1. low = 1,high = 10**7,ans = -1.- While
low <= high: mid = low + (high − low) // 2. - If
canReach(mid): ans = mid, high = mid − 1. Else: low = mid + 1. - Return ans.
6Code (Python)
import math
class Solution:
def minSpeedOnTime(self, dist, hour):
if hour <= len(dist) - 1: # optional base case
return -1
low, high = 1, 10**7
ans = -1 # stays -1 if nothing works
while low <= high:
mid = low + (high - low) // 2 # mid = speed to test
if self.canReach(dist, hour, mid):
ans = mid # on time: remember it
high = mid - 1 # try slower
else:
low = mid + 1 # late: go faster
return ans
def canReach(self, dist, hour, speed):
n = len(dist)
time = 0.0
for j in range(n):
t = dist[j] / speed
if j != n - 1:
time += math.ceil(t) # wait for the next whole hour
else:
time += t # last ride: exact
return time <= hourclass Solution:
def minSpeedOnTime(self, dist, hour):
H = round(hour * 100) # hour in hundredths, e.g. 2.7 -> 270
n = len(dist)
if H <= 100 * (n - 1): # base case: hour <= n - 1
return -1
low, high = 1, 10**7
ans = -1
while low <= high:
mid = low + (high - low) // 2
if self.canReach(dist, H, mid):
ans = mid
high = mid - 1
else:
low = mid + 1
return ans
def canReach(self, dist, H, speed):
full = 0
for j in range(len(dist) - 1): # every ride except the last
full += (dist[j] + speed - 1) // speed # ceil, in whole hours
# full + dist[-1] / speed <= H / 100, multiplied by 100 * speed:
return 100 * (full * speed + dist[-1]) <= H * speed7Code line by line
| line | what it means |
|---|---|
| low, high = 1, 10**7 | 1 = slowest real speed; 10⁷ = the most the statement allows. |
| ans = -1 | The "impossible" answer, used if no speed ever works. |
| while low <= high: | <= so the last single speed is tested too. |
| ans = mid high = mid - 1 | On time: write mid down, then search slower speeds. |
| low = mid + 1 | Late: every speed ≤ mid is late too, search faster. |
| time += math.ceil(t) | (teacher's version) a non-last ride costs whole hours. |
| H = round(hour * 100) | (safe version) 2.7 → 270. round removes tiny errors like 269.99999. |
| full += (dist[j] + speed - 1) // speed | (safe version) same ceiling, done with integers. |
| 100 * (full * speed + dist[-1]) <= H * speed | (safe version) "full + last/speed ≤ hour", with both sides multiplied by 100 × speed so no division is left. |
8Dry run (hand table)
dist = [1, 3, 2], hour = 2.7. low = 1, high = 10⁷, ans = −1.
| step | low | high | mid | time at mid | on time? | decision |
|---|---|---|---|---|---|---|
| 1 | 1 | 10,000,000 | 5,000,000 | 1 + 1 + 0.0000004 ≈ 2.0 | yes | ans = 5,000,000, high = 4,999,999 |
| 2 | 1 | 4,999,999 | 2,500,000 | ≈ 2.0 | yes | ans = 2,500,000, high = 2,499,999 |
| 3–11 | 1 | mid keeps halving: 1,250,000 … 9,765 … 4,882 | ≈ 2.0 | yes | always go left | |
| 12 | 1 | 4,881 | 2,441 | 2.001 | yes | ans = 2441, high = 2440 |
| 13 | 1 | 2,440 | 1,220 | 2.002 | yes | ans = 1220, high = 1219 |
| 14 | 1 | 1,219 | 610 | 2.003 | yes | ans = 610, high = 609 |
| 15 | 1 | 609 | 305 | 2.007 | yes | ans = 305, high = 304 |
| 16 | 1 | 304 | 152 | 2.013 | yes | ans = 152, high = 151 |
| 17 | 1 | 151 | 76 | 2.026 | yes | ans = 76, high = 75 |
| 18 | 1 | 75 | 38 | 2.053 | yes | ans = 38, high = 37 |
| 19 | 1 | 37 | 19 | 2.105 | yes | ans = 19, high = 18 |
| 20 | 1 | 18 | 9 | 1 + 1 + 0.222 = 2.222 | yes | ans = 9, high = 8 |
| 21 | 1 | 8 | 4 | 1 + 1 + 0.5 = 2.5 | yes | ans = 4, high = 3 |
| 22 | 1 | 3 | 2 | 1 + 2 + 1.0 = 4.0 | no | low = 3 (1 … 2 thrown away) |
| 23 | 3 | 3 | 3 | 1 + 1 + 0.667 = 2.667 | yes | ans = 3, high = 2 |
| end | 3 | 2 | low > high → return 3 ✓ (23 checks instead of 3 here, but 24 at most even when the answer is near 10⁷) | |||
The last few steps on the speeds 1 … 8 (yellow = mid, grey = thrown away):
With hour = 6, the same walk continues one step further: speed 2 also works (4.0 ≤ 6), and then speed 1 (6.0 ≤ 6), so the answer is 1. With hour = 1.9, the base case returns −1 at once (and without it, every check says "no", so ans stays −1).
9Complexity & remember
- Time O(n × log(10⁷)) ≈ 24 × 10⁵ = 2.4 × 10⁶ → passes.
- Space O(1).
Part C · Revision page
| Brute force | Binary search on answer | |
|---|---|---|
| range | 1 … 10⁷ | 1 … 10⁷ |
| walk | speed by speed, upward | test mid, drop half |
| check | Σ ceil(dist/speed) over the first n − 1 trains + exact last ride ≤ hour | |
| impossible | return −1 after the loop | ans stays −1 |
| time | 10⁷ × 10⁵ = 10¹² → TLE | 24 × 10⁵ ✓ |
| Koko (14) | Ship packages (15) | Min speed (16) | |
|---|---|---|---|
| low | 1 | max(weights) | 1 |
| high | max(piles) | sum(weights) | 10⁷ (given in the statement) |
| rounding | ceil every pile | none (whole packages) | ceil every ride except the last |
| can be impossible? | no | no | yes → −1 |
2. Every ride except the last costs ceil(dist / speed) hours; the last costs dist / speed exactly.
3. On time → ans = mid, high = mid − 1. Late → low = mid + 1.
4. ans starts at −1, so "impossible" comes out on its own (or check hour ≤ n − 1 first).
5. For exact answers, compare in hundredths with integers, not with floats.
✗ using max(dist) as high (the decimal last ride can need up to 10⁷)
✗ integer division in Java/C++ (
dist/speed without a cast to double)✗
while low < high with the "save ans" style (the last speed is never tested)✗ trusting
float <= float at the exact boundary (dist = [50, 7], hour = 1.14)s = Solution() print(s.minSpeedOnTime([1, 3, 2], 6)) # 1 print(s.minSpeedOnTime([1, 3, 2], 2.7)) # 3 print(s.minSpeedOnTime([1, 3, 2], 1.9)) # -1 print(s.minSpeedOnTime([1, 1, 100000], 2.01)) # 10000000 print(s.minSpeedOnTime([50, 7], 1.14)) # 50 (float version says 51)
Based on this video: Minimum Speed to Arrive on Time | Binary Search on Answer