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 · 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

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

  1. Answer range: pick the smallest possible answer (low) and the largest we'd ever need (high).
  2. Check function: check(speed) = "at this speed, do I reach the office in time?"
  3. 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.
  4. 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.

index012
dist132hour = 6 → 1 · hour = 2.7 → 3 · hour = 1.9 → −1

2What the constraints tell us

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:

Total 2.67 h ≤ 2.7 → on time. (Speed 2 gives 1 + 2 + 1.0 = 4 h, too slow.) Answer 3.

Doubt: why do we round up all the trains except the last?
→ 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

Doubt: why isn't max(dist) enough as high, like in Koko?
→ 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.

Base case (optional)if hour <= n - 1: return -1
She 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

  1. (Optional) if hour <= n - 1 → return −1.
  2. For each speed from 1 to 10⁷:
  3. time = 0.0. For each train j: t = dist[j] / speed. If j is not the last index, add ceil(t); else add t.
  4. If time ≤ hour → return this speed (first one = minimum).
  5. After the loop → return −1.

6Code (Python)

Brute force (teacher's way, TLE on big inputs)
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 -1

7Code line by line

linewhat it means
if hour <= n - 1: return -1Even infinitely fast trains need more than n − 1 hours.
for speed in range(1, 10**7 + 1):Every candidate speed, slowest first.
time = 0.0A 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] / speedExact 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 += tLast train: no waiting after it.
if time <= hour: return speedOn 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 -1No speed in the whole range worked.

8Dry run

dist = [1, 3, 2], hour = 2.7. Base case: 2.7 > 2, so continue.

speedtrain 1train 2train 3 (exact)total≤ 2.7 ?
1132.06.0no
20.5 → 11.5 → 21.04.0no
30.33 → 110.672.67yes → 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

Remember the brute forceRange 1 … 10⁷ (from the statement). Ceil every ride except the last. First speed with time ≤ hour wins; none → −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

Doubt (a fix to the teacher's code): is comparing decimals with 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

  1. (Optional) if hour <= n - 1 → −1.
  2. low = 1, high = 10**7, ans = -1.
  3. While low <= high: mid = low + (high − low) // 2.
  4. If canReach(mid): ans = mid, high = mid − 1. Else: low = mid + 1.
  5. Return ans.

6Code (Python)

Binary search on answer (teacher's version, decimals)
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 <= hour
Safe version (recommended): whole numbers only, no rounding errors
class 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 * speed

7Code line by line

linewhat it means
low, high = 1, 10**71 = slowest real speed; 10⁷ = the most the statement allows.
ans = -1The "impossible" answer, used if no speed ever works.
while low <= high:<= so the last single speed is tested too.
ans = mid high = mid - 1On time: write mid down, then search slower speeds.
low = mid + 1Late: 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.

steplowhighmidtime at midon time?decision
1110,000,0005,000,0001 + 1 + 0.0000004 ≈ 2.0yesans = 5,000,000, high = 4,999,999
214,999,9992,500,000≈ 2.0yesans = 2,500,000, high = 2,499,999
3–111mid keeps halving: 1,250,000 … 9,765 … 4,882≈ 2.0yesalways go left
1214,8812,4412.001yesans = 2441, high = 2440
1312,4401,2202.002yesans = 1220, high = 1219
1411,2196102.003yesans = 610, high = 609
1516093052.007yesans = 305, high = 304
1613041522.013yesans = 152, high = 151
171151762.026yesans = 76, high = 75
18175382.053yesans = 38, high = 37
19137192.105yesans = 19, high = 18
2011891 + 1 + 0.222 = 2.222yesans = 9, high = 8
211841 + 1 + 0.5 = 2.5yesans = 4, high = 3
221321 + 2 + 1.0 = 4.0nolow = 3 (1 … 2 thrown away)
233331 + 1 + 0.667 = 2.667yesans = 3, high = 2
end32low > 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):

speed12345678
step 21123456782.5 ≤ 2.7 → ans = 4, go left
step 22123456784.0 > 2.7 → go right
step 23123456782.67 ≤ 2.7 → ans = 3, done

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

Remember Min Speed on TimeRange 1 … 10⁷ (from the statement, not max/sum). Ceil every ride except the last. ans starts at −1. On time → save, go left. If hour ≤ n − 1 → impossible.

Part C · Revision page

Brute forceBinary search on answer
range1 … 10⁷1 … 10⁷
walkspeed by speed, upwardtest mid, drop half
checkΣ ceil(dist/speed) over the first n − 1 trains + exact last ride ≤ hour
impossiblereturn −1 after the loopans stays −1
time10⁷ × 10⁵ = 10¹² → TLE24 × 10⁵ ✓
Koko (14)Ship packages (15)Min speed (16)
low1max(weights)1
highmax(piles)sum(weights)10⁷ (given in the statement)
roundingceil every pilenone (whole packages)ceil every ride except the last
can be impossible?nonoyes → −1
If you remember only 5 lines 1. Search the speed: low = 1, high = 10⁷ (the statement says so).
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.
Mistakes to avoid ✗ rounding up the last ride too
✗ 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)
test it yourself (paste under the safe Part B solution)
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