Hi👋SpeedAlgo • Deliberate Practice & Cognitive Ergonomics for Software Engineers

An interactive algorithm mastery and technical interview preparation platform published by Hi👋WebEnterprise. Built for senior and staff software engineers preparing for rigorous coding screens at top tech companies (FAANG/MAMAA).

13 Core Algorithmic Patterns & 225 Practice Problems

  • 1. Two Pointers (16 Paradigms, 43 Problems): Opposite ends, sorted pair sums, palindrome verification, trapping rain water, 3Sum, container with most water — includes the Sliding Window and Fast & Slow Pointers paradigms (contiguous subarray invariants, longest substrings, Floyd's cycle detection, linked list middle).
  • 2. Binary Search (9 Paradigms, 14 Problems): Monotonic predicate partitioning (feasible(x)), boundary search, rotated sorted arrays, square roots, minimum ship capacity, median of two sorted arrays, matrix median on value range.
  • 3. Bit Manipulation (5 Paradigms, 11 Problems): Counting set bits, XOR cancellation, bit reversal, bitmask pairing, maximum XOR with a bitwise trie.
  • 4. Math & Geometry (8 Paradigms, 14 Problems): Sieve of Eratosthenes, integer/roman conversions, modular arithmetic, geometric simulation.
  • 5. Tree/Graph Depth-First Search (15 Paradigms, 24 Problems): Path sums, lowest common ancestor, tree diameter, validating BSTs, tree DP (House Robber III, Binary Tree Cameras).
  • 6. Tree/Graph Breadth-First Search (5 Paradigms, 12 Problems): Level-order traversals, shortest path in unweighted graphs, zig-zag traversals, rotting oranges, word ladders.
  • 7. Graphs (12 Paradigms, 22 Problems): Topological sort, cycle detection, Dijkstra's algorithm, bipartite graph validation, network delay.
  • 8. Backtracking (6 Paradigms, 14 Problems): Subsets, permutations, combinations, constraint satisfaction, pruning, N-Queens, Sudoku solver.
  • 9. Dynamic Programming (13 Paradigms, 28 Problems): Memoization vs tabulation, 0/1 knapsack, unbounded knapsack, coin change, edit distance, longest common subsequence.
  • 10. Heap / Priority Queue (9 Paradigms, 11 Problems): Running medians, top-k elements, interval scheduling, IPO, k-way merges.
  • 11. Advanced Data Structures (7 Paradigms, 17 Problems): Trie (prefix tree), Union-Find (Disjoint Set Union), LRU Cache, LFU Cache, range-sum queries with updates.
  • 12. Stack (5 Paradigms, 9 Problems): Next greater element, largest rectangle in histogram, remove k digits, decode string, min stack.
  • 13. Intervals & Greedy (2 Paradigms, 6 Problems): Merge intervals, interval scheduling, meeting rooms by sweep line, jump game.

4-Stage Deliberate Practice Framework

  1. Stage 1 (Compare & Learn): Read the pattern's mental model, loop invariant and Big-O proof, compare the pattern's template with the problem's solution side by side, and use the step-by-step debugger. The solution can also be shown in C#, Java, TypeScript, C++, Go or Rust for almost every problem.
  2. Stage 2 (Active Recall): Rebuild the solution from memory in a code editor, with an invariant checklist and optional progressive hints, and check it against a judged test suite.
  3. Stage 3 (AI Interview Coach, coming soon): A mock-interview stage still in development; for now, students go straight from Stage 2 to Stage 4.
  4. Stage 4 (Solve on Your Own): Solve a new problem on your own against a 15-minute timer; your code is graded against a test suite. Python and TypeScript run in your browser and C# runs on our server (design problems grade in Python and TypeScript).

Equipped with a spaced-repetition review hub on a fixed 7-step schedule (1 day up to 90 days), a two-pane Studio workspace, and study notes.

Pricing, Access & Commercial Terms

  • Core Curriculum: 100% Free. No credit card required.
  • Coins: New accounts get 40 free coins once, the daily claim adds +20 coins once a day, and inviting a friend earns you +25 coins when they complete their first practice step. These are the only ways to earn coins.
  • AI Coach: Each AI coach question costs 1-5 coins, priced by the server according to its size.
  • BYOK (Bring Your Own Key): AI coaching is free with your own Google Gemini, OpenAI or OpenRouter API key (BYOK).
  • Extras: Coins also unlock extras for 24 hours: a Recall hint level (1 coin), a Solve solution reveal (2), a LeetCode problem card's solutions (1) and a pattern's extra canonical problems (1).
  • Payments & Refunds: Nothing on the site is sold for money: coins can only be earned, never bought. There are no subscriptions or charges, so there is nothing to refund.
  • Platform Operator: Hi👋WebEnterprise. Support & policies at hispeedalgo.com.
Skip to main content
Hi👋SpeedAlgo

Invariant-First Algorithmic Mastery

Arena
238Items
Theory Context•Binary Search Boundary

Builds on Feasible Function and Lower Bound, which you haven’t finished yet.

P3MediumBuilt on Lower Bound~4 min speedrun

Binary Search on Real Numbers (Bisection Method)

Built on
Lower BoundThe candidates are real numbers, so the loop runs a fixed number of times instead of waiting until lo is equal to hi.

Separate Squares I (LC 3453)(opens leetcode.com)

You get a list of squares on a flat x/y plane. squares[i] = [xi, yi, li] describes square i. Its bottom-left corner is at the point (xi, yi), and each of its four sides has length li. Every side is parallel to the x-axis or to the y-axis.

Now draw a horizontal line y = h. Each square is split into the part under the line and the part over it (a square that lies wholly on one side has an empty part on the other side). The area below the line is the sum of the parts under the line, added up over all squares. The area above the line is the sum of the parts over it. Find the smallest h at which these two sums are equal.

Squares may overlap. The overlap is not merged: every square adds its own area to the sums. So a region that two squares cover is counted twice.

The answer is a real number, so an exact value is not required. Your answer is accepted when it differs from the true answer by at most 10^-5. The number 10^-5 is 1 divided by 100,000, which is 0.00001. For example, the true answer of Example 2 is 7/6 = 1.1666666..., so any value from 1.1666566... to 1.1666766... is accepted, and the printed 1.16667 is one of them.

Example 1
Input:squares = [[0,0,1],[2,2,1]]
Output:1.00000
Square 0 covers heights 0 to 1, and square 1 covers heights 2 to 3. Each square has area 1 * 1 = 1. Every line with 1 <= h <= 2 has all of square 0 below it and all of square 1 above it, so each side holds 1 unit of area. The lowest of these lines is h = 1.
the squares on the x/y plane, cut by the answer line y = h
square 0square 1xy01230123y = 1below the line: 1above the line: 1

Continuous Bisection

real-valued answer (accepted within a tolerance such as 10^-5 = 0.00001) âž” move lo or hi onto mid for a fixed number of rounds âž” the boundary within the tolerance
Spot it when: The statement asks for a real number, such as a height, a time, a distance or a radius, and accepts an answer within an error such as 10^-5. A yes-or-no question about one guess is False for small guesses and True from some value on.
Continuous Bisection
Python
1from typing import Callable, List
2 
3 
4def binary_search_lower_bound_reals(lo: float, hi: float, is_feasible: Callable[[float], bool], rounds: int = 100) -> float:
5 """First True boundary in [lo, hi], approximated up to floating-point precision.
6 In exact arithmetic, width is initial_width / 2**rounds; float rounding limits further shrinkage.
7 Precondition: is_feasible is monotonic (False, False, ..., True, True).
8 Returns hi when no value is True. T = the cost of one is_feasible call.
9 Time: O(T * rounds) | Space: O(1)
10 """
11 for _ in range(rounds):
12 mid = (lo + hi) / 2
13 # 1. Check the Midpoint: is_feasible(mid) shows which side of the boundary mid is on.
14 if is_feasible(mid):
15 hi = mid # 2. Narrow Toward Mid from Above: mid works, so move hi down to mid.
16 else:
17 lo = mid # 3. Narrow Toward Mid from Below: mid fails, so move lo up to mid.
18 
19 # 4. Return the Approximation: after rounds halvings, hi is on the True side within float precision.
20 return hi
Invariant: The first True value is always in (lo, hi]: lo and every value below it are False, and hi is True or is its starting value, so each round halves that range.

A real number has no next value after mid, so move the end onto mid itself: set hi = mid when is_feasible(mid) is True, set lo = mid when it is False, and repeat for rounds rounds. Each round keeps the first True value between lo and hi and halves the width hi - lo, so after rounds rounds the width is initial_width / 2**rounds, until float rounding stops it. hi is the True end, or the starting hi when nothing is True, so return hi. The trap is while lo < hi: when lo and hi become neighbouring floats (two floats with no other float between them), mid equals one of them, hi = mid or lo = mid changes nothing, and the loop never ends.

See it run

Continuous Bisection: Lower Bound over real numbers, step by step

round 0 / 8 · lo 0 · hi 2 · mid none

round 1 / 8 · lo 0 · hi 2 · mid 1

round 2 / 8 · lo 1 · hi 2 · mid 1.5

round 3 / 8 · lo 1 · hi 1.5 · mid 1.25

round 4 / 8 · lo 1 · hi 1.25 · mid 1.125

round 5 / 8 · lo 1.125 · hi 1.25 · mid 1.1875

round 6 / 8 · lo 1.125 · hi 1.1875 · mid 1.1563

round 7 / 8 · lo 1.1563 · hi 1.1875 · mid 1.1719

round 8 / 8 · lo 1.1563 · hi 1.1719 · mid 1.1641

round 8 / 8 · lo 1.1641 · hi 1.1719 · mid none · answer 1.17188

Each column is one round of the loop. The last column holds the final lo and hi.

  • lo the end of the range that fails the check
  • hi the end of the range that passes the check
  • mid the position the check reads now
  • check True when at least half of the area is below mid
  • answer the final hi, which always passes the check

Squares [[0,0,2],[1,1,1]]. lo = 0 is the lowest bottom edge, hi = 2 is the highest top edge, and the loop runs 8 rounds.

mid = (0 + 2) / 2 = 1. Less than half of the area is below it, so the check is False and lo = 1, not mid + 1.

mid = (1 + 2) / 2 = 1.5. At least half of the area is below it, so the check is True and hi = 1.5.

mid = (1 + 1.5) / 2 = 1.25. At least half of the area is below it, so the check is True and hi = 1.25.

mid = (1 + 1.25) / 2 = 1.125. Less than half of the area is below it, so the check is False and lo = 1.125, not mid + 1.

mid = (1.125 + 1.25) / 2 = 1.1875. At least half of the area is below it, so the check is True and hi = 1.1875.

mid = (1.125 + 1.1875) / 2 = 1.1563. Less than half of the area is below it, so the check is False and lo = 1.1563, not mid + 1.

mid = (1.1563 + 1.1875) / 2 = 1.1719. At least half of the area is below it, so the check is True and hi = 1.1719.

mid = (1.1563 + 1.1719) / 2 = 1.1641. Less than half of the area is below it, so the check is False and lo = 1.1641, not mid + 1.

The loop stops after 8 rounds, not when lo equals hi. hi = 1.1719 passes the check, so it is the answer, within 0.0078.

lo
1
2
3
4
5
6
7
8
end
hi
mid
check
1 / 10
Gotchas1 of 7

Gotcha 1 of 7

  1. 1
    The trap

    Gotcha 1: Looping while lo < hi on floats

    Mistake
    while lo < hi:
    Fix
    for _ in range(rounds):

    What happens: Between two floats there is only a limited number of other floats. After enough rounds, lo and hi are neighbouring floats. In the example, this happens after 54 rounds, with lo = 0.9999999999999999 and hi = 1.0. Then mid = (lo + hi) / 2 equals hi, the check passes, and hi = mid changes nothing. So lo < hi stays True forever. A fixed number of rounds always ends.

    Example: Separate Squares I (LC 3453): squares = [[0, 0, 1], [2, 2, 1]]
    Mistake: the loop never endsFix: returns 1.0
  2. 2

    Gotcha 2: Using too few rounds

    Mistake
    rounds = 10
    Fix
    rounds = 100

    What happens: Each round halves the range, so 10 rounds shrink it only by a factor of 1024. On [0, 106] the width after 10 rounds is about 977, so the answer can be off by hundreds. With 100 rounds the search reaches floating-point precision. Further rounds may leave the ends unchanged.

    Example: a boundary at 123456.789 on [0, 10^6]
    Mistake: returns 124023.4375Fix: returns 123456.789
  3. 3

    Gotcha 3: Dividing with // instead of /

    Mistake
    mid = (lo + hi) // 2
    Fix
    mid = (lo + hi) / 2

    What happens: // rounds the midpoint down to a whole number. In the example, lo = 0 and hi = 2, so the first mid is 1. The check is False, so lo = 1. The next mid is (1 + 2) // 2 = 1 again, so lo stays 1 in every later round. hi stays 2, and the call returns 2 instead of the boundary between 1 and 2.

    Example: Separate Squares I (LC 3453): squares = [[0, 0, 2], [1, 1, 1]]
    Mistake: returns 2Fix: returns 1.1666666666666667
  4. 4

    Gotcha 4: Keeping the integer lo = mid + 1

    Mistake
    lo = mid + 1
    Fix
    lo = mid

    What happens: A real number has no next value, so mid + 1 can jump far past the answer. In the example, the boundary is 0.3 on [0, 1]. Round 1: mid = 0.5 is True, so hi = 0.5. Round 2: mid = 0.25 is False, so lo = 0.25 + 1 = 1.25, which is above hi. From then on every mid is above 0.3, so every check is True and hi = mid runs in every round. hi moves toward lo and ends at 1.25.

    Example: a boundary at 0.3 on [0, 1]
    Mistake: returns 1.25Fix: returns 0.3
  5. 5

    Gotcha 5: Returning lo instead of hi

    Mistake
    return lo
    Fix
    return hi

    What happens: hi is the True end and lo is the False end. After the last round they are neighbouring floats, and the first True value is the upper one. In the example, the exact answer is 1.0. lo ends at 0.9999999999999999, the float just below 1.0. At that line the area below is 0.9999999999999999, which is less than 1, half of the total area 2, so the check is False there. The call returns a value that fails the check.

    Example: Separate Squares I (LC 3453): squares = [[0, 0, 1], [2, 2, 1]]
    Mistake: returns 0.9999999999999999Fix: returns 1.0
  6. 6

    Gotcha 6: Adding every area into one decimal sum

    Mistake
    if line_y >= y + side:
        area_below += side * side
    elif line_y > y:
        area_below += side * (line_y - y)
    Fix
    if line_y >= y + side:
        full_area += side * side
    elif line_y > y:
        partial_area += side * (line_y - y)

    What happens: A float keeps only about 16 significant digits. The whole squares can add up to a huge area, around 1011 here. At that size, the smallest step a float can hold is about 0.00006. When the small slice of a thin square is added to that sum, it is rounded away. The check then gives the wrong answer for lines near that slice. Keep the whole squares as an exact whole number in full_area, and let only the square the line crosses be a decimal in partial_area.

    Example: Separate Squares I (LC 3453): squares = [[0, 1000000, 1], [0, 0, 600000], [0, 100000000, 600000], [0, 1000001, 1]]
    Mistake: returns 1000000.9999694824Fix: returns 1000001.0
  7. 7

    Gotcha 7: Writing > instead of >= in the area check

    Mistake
    return partial_area > total_area / 2 - full_area
    Fix
    return partial_area >= total_area / 2 - full_area

    What happens: In the example, the total area is 2, so half is 1. Every line from y = 1 to y = 2 has exactly area 1 below it. With >=, the check is True on those lines, so hi = mid moves down and the search returns the lowest line, 1.0. With >, the check is False on those lines, because 1 is not greater than 1. So lo = mid moves up, and the first True line is just above 2. The call returns 2.0000000000000004, and the problem asks for the lowest line.

    Example: Separate Squares I (LC 3453): squares = [[0, 0, 1], [2, 2, 1]]
    Mistake: returns 2.0000000000000004Fix: returns 1.0
Not this one if…
  • The answer must be a whole number, the piece length in Cutting Ribbons (LC 1891): every candidate has a next one, so the integer loop ends exactly on the answer, with no tolerance or round count to reason about. See Feasible Function
  • The values being ranked are whole numbers, the pair distances in Find K-th Smallest Pair Distance (LC 719): the k-th smallest is an integer the value search lands on exactly with lo = mid + 1, and the answer is an integer, so the integer loop ends on it. See Kth Smallest

Theory

The question we build

What we know

The area below a horizontal line never gets smaller when the line rises. So the question "is at least half of the area below this line?" is False for low lines and True from some line on.

What the problem gives us

LeetCode accepts an answer within 10^-5. So the search needs a final range narrower than 10^-5, not the exact value.

The question we build

We use squares = [[0, 0, 2], [1, 1, 1]]. The total area is 4 + 1 = 5, so half of it is 2.5. The lowest bottom edge is 0 and the highest top edge is 2, so we try the lines y = 0, 1, 2. The area below them is 0, 2, 5, so the answers are False, False, True. The boundary is between y = 1 and y = 2. The exact answer is 7/6 = 1.16667.

The check at_least_half_below(y) for each candidate: 0 is False, 1 is False, 2 is True.
y
0
1
2
at_least_half_below(y)
F
F
T

The sample lines are whole numbers. The boundary is a real number between y = 1 and y = 2.

Narrowing down to the template

The first True line is the answer, so the call is binary_search_lower_bound_reals(lo, hi, at_least_half_below). At lo, the lowest bottom edge, no area is below the line, so the check is False. At hi, the highest top edge, all the area is below the line, so the check is True.

Step 1: Try values on a grid

The answer is the first value where the check is True. A real number has no next value to step to, so a scan can only try values on a grid.

Step 1: Try values on a gridPython
1def first_true_scan_reals(lo, hi, is_feasible, step=0.001):
2 y = lo
3 while y < hi and not is_feasible(y):
4 y += step
5 return y

Cost: O((hi - lo) / step) checks, and the answer is correct only to step. For a boundary at 0.3 on [0, 1] it takes about 300 checks.

Step 2: Halve the range for a fixed number of rounds

Halving keeps the first True value between lo and hi, as in Lower Bound. The table lists the five changes that real numbers need.

Step 2: Halve the range for a fixed number of roundsPython
1def binary_search_lower_bound_reals(lo: float, hi: float, is_feasible: Callable[[float], bool], rounds: int = 100) -> float:
2 """First True boundary in [lo, hi], approximated up to floating-point precision.
3 In exact arithmetic, width is initial_width / 2**rounds; float rounding limits further shrinkage.
4 Precondition: is_feasible is monotonic (False, False, ..., True, True).
5 Returns hi when no value is True. T = the cost of one is_feasible call.
6 Time: O(T * rounds) | Space: O(1)
7 """
8 for _ in range(rounds):
9 mid = (lo + hi) / 2
10 # 1. Check the Midpoint: is_feasible(mid) shows which side of the boundary mid is on.
11 if is_feasible(mid):
12 hi = mid # 2. Narrow Toward Mid from Above: mid works, so move hi down to mid.
13 else:
14 lo = mid # 3. Narrow Toward Mid from Below: mid fails, so move lo up to mid.
15 
16 # 4. Return the Approximation: after rounds halvings, hi is on the True side within float precision.
17 return hi

Cost: O(T * rounds), where T is the cost of one call of is_feasible.

ItemLower Bound (whole numbers)Continuous Bisection
Loopfor _ in range(rounds):A fixed number of rounds replaces while lo < hi.
mid = lo + (hi - lo) // 2mid = (lo + hi) / 2True division: mid is a real number.
When the check is True: hi = midhi = midUnchanged.
When the check is False: lo = mid + 1lo = midThere is no next value to step to.
Result: lo, the exact first Truehi, within a tiny error of the first Truehi is the True end, or the starting hi when nothing is True.

How many rounds

A float is the number type Python uses for decimals. It keeps about 16 significant digits. To reach an error eps on a range of width hi - lo, you need about log2((hi - lo) / eps) rounds. For squares = [[0, 0, 2], [1, 1, 1]] the width is 2 and eps is 10^-5, so about 18 rounds are enough. The call with rounds = 18 returns 1.1666717529296875, which is 5.1e-6 from 7/6. The default of 100 rounds is far more than needed. The extra rounds only cost time, because float rounding stops the width from shrinking.

Stopping by width

A loop while hi - lo > eps also works when eps is larger than the gap between two floats at the size of the values. For eps = 1e-9 on a range of width 10, it takes about 34 rounds. Near 109, two neighbouring floats are about 1.2e-7 apart, so hi - lo never gets below 1e-9 and the loop never ends. A round count always ends.

The largest value that passes

The card returns hi, the end that passes the check. To find the largest value that passes, give the failing end as lo and the passing end as hi, even though lo is then the larger number. The loop does not change. For the check x * x <= 2, the call binary_search_lower_bound_reals(2, 0, check) returns about 1.414, the square root of 2.

Compare the answer with a tolerance

The result is a float close to the exact value, and it is not always the same float. Compare with abs(answer - expected) <= 1e-5, as LeetCode does, not with ==. For squares = [[0, 0, 2], [0, 0, 1]] the exact answer is 5/6. The search returns 0.8333333333333333, and 5 / 6 is 0.8333333333333334, so == is False.

Real-world use

Numerical libraries find the root of a monotonic function by bisection. They start with one point where the function is below zero and one point where it is above zero, and they halve the range between the two points. The first point plays the role of lo, which is False, and the second point plays the role of hi, which is True.

Say it in an interview

"The answer is a real number, so I use Lower Bound over real numbers. The area below a horizontal line only grows as the line rises, so "is at least half of the area below this line?" is False and then True. I start lo at the lowest bottom edge and hi at the highest top edge. At mid, a passing check sets hi = mid and a failing check sets lo = mid, because a real number has no next value. I run a fixed 100 rounds instead of while lo < hi, because neighbouring floats make that loop never end, and I return hi. Each round costs one pass over the N squares, so the time is O(N * rounds) and the space is O(1)."

Common Questions

What if the answer is an exact fraction, as in K-th Smallest Prime Fraction (LC 786)?

hi is a float close to the fraction, not the fraction itself. After the rounds, make one more pass that finds the largest fraction that is at most hi, and compare two fractions with a * d > c * b instead of a / b > c / d. This is exact because the final range is narrower than the smallest gap between two different fractions, which is at least 1 / (3 * 104)^2 there.

Practice

Canonical LeetCode:Separate Squares I (LC 3453)Minimize Max Distance to Gas Station (LC 774)Maximum Average Subarray II (LC 644)Pour Water Between Buckets to Make Water Levels Equal (LC 2137)K-th Smallest Prime Fraction (LC 786)

The older version of this card had a general loop named bisect_reals, and an anchor with the check written inside the loop. This course names the loop binary_search_lower_bound_reals, like the other cards, and writes the check as its own function at_least_half_below.

The classic loop

Old Way
Python
1from typing import Callable, List
2 
3def bisect_reals(lo: float, hi: float, is_feasible: Callable[[float], bool], rounds: int = 100) -> float:
4 """Continuous Bisection: the real number where is_feasible flips, to within float precision.
5 Precondition: lo fails the check, hi passes it, and the check flips only once between them.
6 lo and hi themselves are never checked. For the highest value that passes, give the failing end as lo.
7 R = rounds. T = the cost of one is_feasible call.
8 Time: O(R * T) | Space: O(1)
9 """
10 for _ in range(rounds): # Fixed rounds: a real number has no next value
11 mid = (lo + hi) / 2 # True division: mid is a real number
12 if is_feasible(mid):
13 hi = mid # mid passes: the answer is mid, or closer to lo
14 else:
15 lo = mid # mid fails: move onto mid, never mid + 1
16 
17 return hi # hi always passes: the boundary, far inside 10^-5
18 
19 
20# Separate Squares I (LC 3453): the template above, with its check written inside the loop (the step debugger runs this code).
21def separateSquares(squares: List[List[int]]) -> float:
22 """Separate Squares I: bisect the height y = mid until the window is far narrower than 10^-5.
23 R = 100 rounds, each one pass over the N squares to measure the area below mid.
24 Time: O(N * R) | Space: O(1)
25 """
26 total = sum(l * l for _, _, l in squares) # Area of every square; overlaps count once per square
27 lo = min(y for _, y, _ in squares) # No area is below lo: the check fails here
28 hi = max(y + l for _, y, l in squares) # All area is below hi: the check passes here
29 
30 for _ in range(100): # Fixed rounds: a real number has no next value
31 mid = (lo + hi) / 2 # True division: mid is a real number
32 full = sum(l * l for _, y, l in squares if y + l <= mid) # Squares wholly below mid: an exact integer
33 part = sum(l * (mid - y) for _, y, l in squares if y < mid < y + l) # Squares mid cuts: the only float part
34 if 2 * part >= total - 2 * full: # >=, not >: keeps the lowest balancing line
35 hi = mid # Half the area is below: the answer is mid or lower
36 else:
37 lo = mid # Too low: the answer is above mid (never mid + 1)
38 
39 return hi # The lowest balancing line, well inside 10^-5

From the classic loop to our template

Old Way lineIn our templateWhy
def bisect_reals(lo, hi, is_feasible, rounds=100)def binary_search_lower_bound_reals(lo, hi, is_feasible, rounds=100)The same loop. The new name follows the other cards.
the check inside the loop of separateSquaresdef at_least_half_below(line_y)The same check, in its own function, so the template can call it.
full = sum(...) and part = sum(...)full_area and partial_area, with plain loopsThe same exact whole number and the same decimal. A plain loop shows every step.
if 2 * part >= total - 2 * full:return partial_area >= total_area / 2 - full_areaThe same comparison, moved into the check.

When the classic form is fine

Use it when an interviewer asks for this form, or when the code base already uses it. It is the same search. The template only gives the loop a name and moves the check into a function.

Gotchas of the classic loop

Gotchas1 of 7

Gotcha 1 of 7

  1. 1
    The trap

    Gotcha 1: Adding the whole squares and the cut squares into one float

    Mistake
    total_lower = sum(l * l for _, y, l in squares if y + l <= mid)
    total_lower += sum(l * (mid - y) for _, y, l in squares if y < mid < y + l)
    if 2 * total_lower >= total:
    Fix
    full = sum(l * l for _, y, l in squares if y + l <= mid)
    part = sum(l * (mid - y) for _, y, l in squares if y < mid < y + l)
    if 2 * part >= total - 2 * full:

    What happens: A float keeps only about 16 significant digits. The whole squares can add up to a huge area, around 1011 here. At that size, the smallest step a float can hold is about 0.00006. When the small slice of a cut square, like 0.99997, is added to that sum, it is rounded to 1.0. The check then passes for a mid that is slightly too low. So hi = mid moves the window below the true answer. The loop is correct. The check is wrong. The fix keeps the two kinds of area apart. full holds the squares wholly below mid, and it is an exact integer. part holds only the squares that mid cuts, so it stays small. total - 2 * full is integer arithmetic, so the huge numbers cancel exactly. The float 2 * part is then compared against a small exact number. In the example, the check becomes 2 * part >= 2, which flips exactly at mid = 1000001. A quick test before coding: multiply the largest value in a float sum by 10^-16. The result must be far smaller than the tolerance. Here 1011 * 10^-16 = 10^-5, which is the tolerance itself, so there is no margin.

    Example: Separate Squares I (LC 3453): squares = [[0, 1000000, 1], [0, 0, 600000], [0, 100000000, 600000], [0, 1000001, 1]]
    Mistake: returns 1000000.9999694824Fix: returns 1000001.0
  2. 2

    Gotcha 2: Looping while lo < hi on floats

    Mistake
    while lo < hi:
    Fix
    for _ in range(100):

    What happens: Between two floats there is only a limited number of other floats. After enough rounds, lo and hi are neighbouring floats. Then mid = (lo + hi) / 2 equals lo or hi. In the example, mid equals hi and the check passes. So hi = mid changes nothing, and the loop never ends. for _ in range(100) stops after 100 rounds.

    Example: Separate Squares I (LC 3453): squares = [[0, 0, 1], [2, 2, 1]]
    Mistake: the loop never endsFix: returns 1.0
  3. 3

    Gotcha 3: Stopping on hi - lo > eps with a tiny eps

    Mistake
    while hi - lo > 1e-9:
    Fix
    for _ in range(100):

    What happens: Near 109, two neighbouring floats are about 1.2e-7 apart. So hi - lo can never drop below 1e-9 there, and the loop never ends. A fixed number of rounds always ends. Each round halves the window, so t rounds leave (hi - lo) / 2^t. A window of 2 * 109 needs 48 rounds to get below 10^-5, so 100 rounds is a safe default.

    Example: squares = [[0, 1000000000, 2]]
    Mistake: the loop never endsFix: returns 1000000001.0
  4. 4

    Gotcha 4: Writing > instead of >= in the area check

    Mistake
    if 2 * part > total - 2 * full:
    Fix
    if 2 * part >= total - 2 * full:

    What happens: Sometimes a whole stretch of lines splits the area in half. In the example, every line from y = 1 to y = 2 does. With >=, a balancing mid moves hi down, so the search keeps the lowest line, 1. With >, a balancing mid moves lo up, so it returns the highest line, 2.

    Example: squares = [[0, 0, 1], [2, 2, 1]]
    Mistake: returns 2.0000000000000004Fix: returns 1.0
  5. 5

    Gotcha 5: Not knowing what lo and hi hold at the end

    Mistake
    return lo
    Fix
    return hi

    What happens: The invariant tells you what each end means. lo always fails the check, and hi always passes it. After 100 rounds they are neighbouring floats, so the answer sits between them. hi is the lowest value known to pass. lo is the highest value known to fail. For "find the smallest h that balances", return hi. Both ends are within 10^-5 of the answer, so lo is usually accepted too. But lo is the failing side, and it lands slightly below the answer. return mid is worse. mid is only the last guess, and it may be on either side. If the problem asks for the highest value that passes, the roles swap. The passing end is then the lower number, and you return that one.

    Example: Separate Squares I (LC 3453): squares = [[0, 0, 1], [2, 2, 1]]
    Mistake: returns 0.9999999999999999Fix: returns 1.0
  6. 6

    Gotcha 6: Returning hi when the answer is an exact fraction

    Mistake
    return hi
    Fix
    best = [0, 1]
    i = 0
    for j in range(1, n):
        while i < j and arr[i] <= hi * arr[j]:
            i += 1
        if i > 0 and arr[i - 1] * best[1] > best[0] * arr[j]:
            best = [arr[i - 1], arr[j]]
    return best

    What happens: return hi gives a float close to the answer, not the answer itself. K-th Smallest Prime Fraction (LC 786) asks for the fraction as [numerator, denominator]. The standard way to get it is one more pass after the rounds. It finds the largest fraction that is at most hi. best = [0, 1] starts at the fraction 0 / 1, which is smaller than every fraction in the input. For each denominator arr[j], i moves forward while arr[i] <= hi * arr[j]. Then arr[i - 1] / arr[j] is the largest fraction with denominator arr[j] that is at most hi. It replaces best when it is larger, and the comparison arr[i - 1] * best[1] > best[0] * arr[j] multiplies integers, so it is exact. The pass is safe because the final window is narrower than the smallest gap between two different fractions. There that gap is at least 1 / (3 * 104)^2.

    Example: K-th Smallest Prime Fraction (LC 786): arr = [1, 2, 3, 5], k = 3
    Mistake: returns 0.4Fix: returns [2, 5]
  7. 7

    Gotcha 7: Comparing two fractions with / instead of *

    Mistake
    if arr[i - 1] / arr[j] > best[0] / best[1]:
    Fix
    if arr[i - 1] * best[1] > best[0] * arr[j]:

    What happens: a / b > c / d rounds each fraction to a float before it compares them. A float keeps only about 16 significant digits. So two fractions that differ by less than that become the same float, or swap order. a * d > c * b asks the same question, because b and d are positive. It multiplies integers, and Python integers are exact at any size. So the answer is always right. In K-th Smallest Prime Fraction (LC 786), two different fractions differ by at least 1 / (3 * 104)^2, so / happens to work there. * needs no such check. Also * never divides by zero. The same rule does not make arr[i] <= mid * arr[j] exact when mid is a float. There mid * arr[j] is rounded once, just like arr[i] / arr[j]. That is why the last step compares the fractions as integers.

    Example: a / b = (10^16 + 1) / 10^16 against c / d = 1 / 1
    Mistake: returns FalseFix: returns True

Earlier signals

The answer is a real number, not an integer or an index. It can be a height, a distance, a water level or an average, which can fall between any two values. The problem accepts the answer within a tolerance such as 10^-5. An answer counts as correct when it differs from the true value by at most 0.00001. The answer can also be a fraction you must rank. A guess is easy to check, and the check flips only once as the guess grows.

Earlier invariant

lo always fails the check and hi always passes it, so the answer is always between them, and each round halves that window.

Earlier direction

real-valued answer (accepted within a tolerance such as 10^-5 = 0.00001, or a ratio or fraction) âž” move lo or hi onto mid for a fixed number of rounds âž” answer within the tolerance

Earlier description

A real number has no next value after mid. So lo = mid + 1 and hi = mid - 1 have no value to step to. Instead, move one end onto mid itself. Set hi = mid when is_feasible(mid) passes, and set lo = mid when it fails. In the code for Separate Squares I (LC 3453), the check is 2 * part >= total - 2 * full. It passes when at least half of the total area is below the line y = mid. lo always fails the check and hi always passes it. So the answer never leaves the window between lo and hi. Each round halves that window. So a fixed number of rounds, for _ in range(100), makes the window far narrower than 10^-5. 10^-5 is 0.00001, the largest error the problem accepts. The same loop also finds the highest value that passes. For that, give the failing end as lo, even though it is the larger number. The trap is the integer loop while lo < hi, which never ends on floats. After enough rounds, lo and hi are neighbouring floats. Then mid equals lo or hi, so hi = mid or lo = mid changes nothing.

Earlier look-alikes

The values being ranked are whole numbers, the pair distances in Find K-th Smallest Pair Distance (LC 719): the k-th smallest is an integer the value search lands on exactly with lo = mid + 1, and there is nothing to snap.

Earlier template

Earlier template
Python
1from typing import Callable, List
2 
3def bisect_reals(lo: float, hi: float, is_feasible: Callable[[float], bool], rounds: int = 100) -> float:
4 """Continuous Bisection: the real number where is_feasible flips, to within float precision.
5 Precondition: lo fails the check, hi passes it, and the check flips only once between them.
6 lo and hi themselves are never checked. For the highest value that passes, give the failing end as lo.
7 R = rounds. T = the cost of one is_feasible call.
8 Time: O(R * T) | Space: O(1)
9 """
10 for _ in range(rounds): # Fixed rounds: a real number has no next value
11 mid = (lo + hi) / 2 # True division: mid is a real number
12 if is_feasible(mid):
13 hi = mid # mid passes: the answer is mid, or closer to lo
14 else:
15 lo = mid # mid fails: move onto mid, never mid + 1
16 
17 return hi # hi always passes: the boundary, far inside 10^-5
18 
19 
20# Separate Squares I (LC 3453): the template above, with its check written inside the loop (the step debugger runs this code).
21def separateSquares(squares: List[List[int]]) -> float:
22 """Separate Squares I: bisect the height y = mid until the window is far narrower than 10^-5.
23 R = 100 rounds, each one pass over the N squares to measure the area below mid.
24 Time: O(N * R) | Space: O(1)
25 """
26 total = sum(l * l for _, _, l in squares) # Area of every square; overlaps count once per square
27 lo = min(y for _, y, _ in squares) # No area is below lo: the check fails here
28 hi = max(y + l for _, y, l in squares) # All area is below hi: the check passes here
29 
30 for _ in range(100): # Fixed rounds: a real number has no next value
31 mid = (lo + hi) / 2 # True division: mid is a real number
32 full = sum(l * l for _, y, l in squares if y + l <= mid) # Squares wholly below mid: an exact integer
33 part = sum(l * (mid - y) for _, y, l in squares if y < mid < y + l) # Squares mid cuts: the only float part
34 if 2 * part >= total - 2 * full: # >=, not >: keeps the lowest balancing line
35 hi = mid # Half the area is below: the answer is mid or lower
36 else:
37 lo = mid # Too low: the answer is above mid (never mid + 1)
38 
39 return hi # The lowest balancing line, well inside 10^-5

Earlier gotchas

Adding the whole squares and the cut squares into one float

A float keeps only about 16 significant digits. The whole squares can add up to a huge area, around 1011 here. At that size, the smallest step a float can hold is about 0.00006. When the small slice of a cut square, like 0.99997, is added to that sum, it is rounded to 1.0. The check then passes for a mid that is slightly too low. So hi = mid moves the window below the true answer. The loop is correct. The check is wrong. The fix keeps the two kinds of area apart. full holds the squares wholly below mid, and it is an exact integer. part holds only the squares that mid cuts, so it stays small. total - 2 * full is integer arithmetic, so the huge numbers cancel exactly. The float 2 * part is then compared against a small exact number. In the example, the check becomes 2 * part >= 2, which flips exactly at mid = 1000001. A quick test before coding: multiply the largest value in a float sum by 10^-16. The result must be far smaller than the tolerance. Here 1011 * 10^-16 = 10^-5, which is the tolerance itself, so there is no margin.

Looping while lo < hi on floats

Between two floats there is only a limited number of other floats. After enough rounds, lo and hi are neighbouring floats. Then mid = (lo + hi) / 2 equals lo or hi. In the example, mid equals hi and the check passes. So hi = mid changes nothing, and the loop never ends. for _ in range(100) stops after 100 rounds.

Stopping on hi - lo > eps with a tiny eps

Near 109, two neighbouring floats are about 1.2e-7 apart. So hi - lo can never drop below 1e-9 there, and the loop never ends. A fixed number of rounds always ends. Each round halves the window, so t rounds leave (hi - lo) / 2^t. A window of 2 * 109 needs 48 rounds to get below 10^-5, so 100 rounds is a safe default.

Writing > instead of >= in the area check

Sometimes a whole stretch of lines splits the area in half. In the example, every line from y = 1 to y = 2 does. With >=, a balancing mid moves hi down, so the search keeps the lowest line, 1. With >, a balancing mid moves lo up, so it returns the highest line, 2.

Not knowing what lo and hi hold at the end

The invariant tells you what each end means. lo always fails the check, and hi always passes it. After 100 rounds they are neighbouring floats, so the answer sits between them. hi is the lowest value known to pass. lo is the highest value known to fail. For "find the smallest h that balances", return hi. Both ends are within 10^-5 of the answer, so lo is usually accepted too. But lo is the failing side, and it lands slightly below the answer. return mid is worse. mid is only the last guess, and it may be on either side. If the problem asks for the highest value that passes, the roles swap. The passing end is then the lower number, and you return that one.

Returning hi when the answer is an exact fraction

return hi gives a float close to the answer, not the answer itself. K-th Smallest Prime Fraction (LC 786) asks for the fraction as [numerator, denominator]. The standard way to get it is one more pass after the rounds. It finds the largest fraction that is at most hi. best = [0, 1] starts at the fraction 0 / 1, which is smaller than every fraction in the input. For each denominator arr[j], i moves forward while arr[i] <= hi * arr[j]. Then arr[i - 1] / arr[j] is the largest fraction with denominator arr[j] that is at most hi. It replaces best when it is larger, and the comparison arr[i - 1] * best[1] > best[0] * arr[j] multiplies integers, so it is exact. The pass is safe because the final window is narrower than the smallest gap between two different fractions. There that gap is at least 1 / (3 * 104)^2.

Comparing two fractions with / instead of *

a / b > c / d rounds each fraction to a float before it compares them. A float keeps only about 16 significant digits. So two fractions that differ by less than that become the same float, or swap order. a * d > c * b asks the same question, because b and d are positive. It multiplies integers, and Python integers are exact at any size. So the answer is always right. In K-th Smallest Prime Fraction (LC 786), two different fractions differ by at least 1 / (3 * 104)^2, so / happens to work there. * needs no such check. Also * never divides by zero. The same rule does not make arr[i] <= mid * arr[j] exact when mid is a float. There mid * arr[j] is rounded once, just like arr[i] / arr[j]. That is why the last step compares the fractions as integers.

Theory Context•Binary Search Boundary

Builds on Feasible Function and Lower Bound, which you haven’t finished yet.

P3MediumBuilt on Lower Bound~4 min speedrun

Binary Search on Real Numbers (Bisection Method)

Built on
Lower BoundThe candidates are real numbers, so the loop runs a fixed number of times instead of waiting until lo is equal to hi.

Separate Squares I (LC 3453)(opens leetcode.com)

You get a list of squares on a flat x/y plane. squares[i] = [xi, yi, li] describes square i. Its bottom-left corner is at the point (xi, yi), and each of its four sides has length li. Every side is parallel to the x-axis or to the y-axis.

Now draw a horizontal line y = h. Each square is split into the part under the line and the part over it (a square that lies wholly on one side has an empty part on the other side). The area below the line is the sum of the parts under the line, added up over all squares. The area above the line is the sum of the parts over it. Find the smallest h at which these two sums are equal.

Squares may overlap. The overlap is not merged: every square adds its own area to the sums. So a region that two squares cover is counted twice.

The answer is a real number, so an exact value is not required. Your answer is accepted when it differs from the true answer by at most 10^-5. The number 10^-5 is 1 divided by 100,000, which is 0.00001. For example, the true answer of Example 2 is 7/6 = 1.1666666..., so any value from 1.1666566... to 1.1666766... is accepted, and the printed 1.16667 is one of them.

Example 1
Input:squares = [[0,0,1],[2,2,1]]
Output:1.00000
Square 0 covers heights 0 to 1, and square 1 covers heights 2 to 3. Each square has area 1 * 1 = 1. Every line with 1 <= h <= 2 has all of square 0 below it and all of square 1 above it, so each side holds 1 unit of area. The lowest of these lines is h = 1.
the squares on the x/y plane, cut by the answer line y = h
square 0square 1xy01230123y = 1below the line: 1above the line: 1

Continuous Bisection

real-valued answer (accepted within a tolerance such as 10^-5 = 0.00001) âž” move lo or hi onto mid for a fixed number of rounds âž” the boundary within the tolerance
Spot it when: The statement asks for a real number, such as a height, a time, a distance or a radius, and accepts an answer within an error such as 10^-5. A yes-or-no question about one guess is False for small guesses and True from some value on.
Continuous Bisection
Python
1from typing import Callable, List
2 
3 
4def binary_search_lower_bound_reals(lo: float, hi: float, is_feasible: Callable[[float], bool], rounds: int = 100) -> float:
5 """First True boundary in [lo, hi], approximated up to floating-point precision.
6 In exact arithmetic, width is initial_width / 2**rounds; float rounding limits further shrinkage.
7 Precondition: is_feasible is monotonic (False, False, ..., True, True).
8 Returns hi when no value is True. T = the cost of one is_feasible call.
9 Time: O(T * rounds) | Space: O(1)
10 """
11 for _ in range(rounds):
12 mid = (lo + hi) / 2
13 # 1. Check the Midpoint: is_feasible(mid) shows which side of the boundary mid is on.
14 if is_feasible(mid):
15 hi = mid # 2. Narrow Toward Mid from Above: mid works, so move hi down to mid.
16 else:
17 lo = mid # 3. Narrow Toward Mid from Below: mid fails, so move lo up to mid.
18 
19 # 4. Return the Approximation: after rounds halvings, hi is on the True side within float precision.
20 return hi
Invariant: The first True value is always in (lo, hi]: lo and every value below it are False, and hi is True or is its starting value, so each round halves that range.

A real number has no next value after mid, so move the end onto mid itself: set hi = mid when is_feasible(mid) is True, set lo = mid when it is False, and repeat for rounds rounds. Each round keeps the first True value between lo and hi and halves the width hi - lo, so after rounds rounds the width is initial_width / 2**rounds, until float rounding stops it. hi is the True end, or the starting hi when nothing is True, so return hi. The trap is while lo < hi: when lo and hi become neighbouring floats (two floats with no other float between them), mid equals one of them, hi = mid or lo = mid changes nothing, and the loop never ends.

See it run

Continuous Bisection: Lower Bound over real numbers, step by step

round 0 / 8 · lo 0 · hi 2 · mid none

round 1 / 8 · lo 0 · hi 2 · mid 1

round 2 / 8 · lo 1 · hi 2 · mid 1.5

round 3 / 8 · lo 1 · hi 1.5 · mid 1.25

round 4 / 8 · lo 1 · hi 1.25 · mid 1.125

round 5 / 8 · lo 1.125 · hi 1.25 · mid 1.1875

round 6 / 8 · lo 1.125 · hi 1.1875 · mid 1.1563

round 7 / 8 · lo 1.1563 · hi 1.1875 · mid 1.1719

round 8 / 8 · lo 1.1563 · hi 1.1719 · mid 1.1641

round 8 / 8 · lo 1.1641 · hi 1.1719 · mid none · answer 1.17188

Each column is one round of the loop. The last column holds the final lo and hi.

  • lo the end of the range that fails the check
  • hi the end of the range that passes the check
  • mid the position the check reads now
  • check True when at least half of the area is below mid
  • answer the final hi, which always passes the check

Squares [[0,0,2],[1,1,1]]. lo = 0 is the lowest bottom edge, hi = 2 is the highest top edge, and the loop runs 8 rounds.

mid = (0 + 2) / 2 = 1. Less than half of the area is below it, so the check is False and lo = 1, not mid + 1.

mid = (1 + 2) / 2 = 1.5. At least half of the area is below it, so the check is True and hi = 1.5.

mid = (1 + 1.5) / 2 = 1.25. At least half of the area is below it, so the check is True and hi = 1.25.

mid = (1 + 1.25) / 2 = 1.125. Less than half of the area is below it, so the check is False and lo = 1.125, not mid + 1.

mid = (1.125 + 1.25) / 2 = 1.1875. At least half of the area is below it, so the check is True and hi = 1.1875.

mid = (1.125 + 1.1875) / 2 = 1.1563. Less than half of the area is below it, so the check is False and lo = 1.1563, not mid + 1.

mid = (1.1563 + 1.1875) / 2 = 1.1719. At least half of the area is below it, so the check is True and hi = 1.1719.

mid = (1.1563 + 1.1719) / 2 = 1.1641. Less than half of the area is below it, so the check is False and lo = 1.1641, not mid + 1.

The loop stops after 8 rounds, not when lo equals hi. hi = 1.1719 passes the check, so it is the answer, within 0.0078.

lo
1
2
3
4
5
6
7
8
end
hi
mid
check
1 / 10
Gotchas1 of 7

Gotcha 1 of 7

  1. 1
    The trap

    Gotcha 1: Looping while lo < hi on floats

    Mistake
    while lo < hi:
    Fix
    for _ in range(rounds):

    What happens: Between two floats there is only a limited number of other floats. After enough rounds, lo and hi are neighbouring floats. In the example, this happens after 54 rounds, with lo = 0.9999999999999999 and hi = 1.0. Then mid = (lo + hi) / 2 equals hi, the check passes, and hi = mid changes nothing. So lo < hi stays True forever. A fixed number of rounds always ends.

    Example: Separate Squares I (LC 3453): squares = [[0, 0, 1], [2, 2, 1]]
    Mistake: the loop never endsFix: returns 1.0
  2. 2

    Gotcha 2: Using too few rounds

    Mistake
    rounds = 10
    Fix
    rounds = 100

    What happens: Each round halves the range, so 10 rounds shrink it only by a factor of 1024. On [0, 106] the width after 10 rounds is about 977, so the answer can be off by hundreds. With 100 rounds the search reaches floating-point precision. Further rounds may leave the ends unchanged.

    Example: a boundary at 123456.789 on [0, 10^6]
    Mistake: returns 124023.4375Fix: returns 123456.789
  3. 3

    Gotcha 3: Dividing with // instead of /

    Mistake
    mid = (lo + hi) // 2
    Fix
    mid = (lo + hi) / 2

    What happens: // rounds the midpoint down to a whole number. In the example, lo = 0 and hi = 2, so the first mid is 1. The check is False, so lo = 1. The next mid is (1 + 2) // 2 = 1 again, so lo stays 1 in every later round. hi stays 2, and the call returns 2 instead of the boundary between 1 and 2.

    Example: Separate Squares I (LC 3453): squares = [[0, 0, 2], [1, 1, 1]]
    Mistake: returns 2Fix: returns 1.1666666666666667
  4. 4

    Gotcha 4: Keeping the integer lo = mid + 1

    Mistake
    lo = mid + 1
    Fix
    lo = mid

    What happens: A real number has no next value, so mid + 1 can jump far past the answer. In the example, the boundary is 0.3 on [0, 1]. Round 1: mid = 0.5 is True, so hi = 0.5. Round 2: mid = 0.25 is False, so lo = 0.25 + 1 = 1.25, which is above hi. From then on every mid is above 0.3, so every check is True and hi = mid runs in every round. hi moves toward lo and ends at 1.25.

    Example: a boundary at 0.3 on [0, 1]
    Mistake: returns 1.25Fix: returns 0.3
  5. 5

    Gotcha 5: Returning lo instead of hi

    Mistake
    return lo
    Fix
    return hi

    What happens: hi is the True end and lo is the False end. After the last round they are neighbouring floats, and the first True value is the upper one. In the example, the exact answer is 1.0. lo ends at 0.9999999999999999, the float just below 1.0. At that line the area below is 0.9999999999999999, which is less than 1, half of the total area 2, so the check is False there. The call returns a value that fails the check.

    Example: Separate Squares I (LC 3453): squares = [[0, 0, 1], [2, 2, 1]]
    Mistake: returns 0.9999999999999999Fix: returns 1.0
  6. 6

    Gotcha 6: Adding every area into one decimal sum

    Mistake
    if line_y >= y + side:
        area_below += side * side
    elif line_y > y:
        area_below += side * (line_y - y)
    Fix
    if line_y >= y + side:
        full_area += side * side
    elif line_y > y:
        partial_area += side * (line_y - y)

    What happens: A float keeps only about 16 significant digits. The whole squares can add up to a huge area, around 1011 here. At that size, the smallest step a float can hold is about 0.00006. When the small slice of a thin square is added to that sum, it is rounded away. The check then gives the wrong answer for lines near that slice. Keep the whole squares as an exact whole number in full_area, and let only the square the line crosses be a decimal in partial_area.

    Example: Separate Squares I (LC 3453): squares = [[0, 1000000, 1], [0, 0, 600000], [0, 100000000, 600000], [0, 1000001, 1]]
    Mistake: returns 1000000.9999694824Fix: returns 1000001.0
  7. 7

    Gotcha 7: Writing > instead of >= in the area check

    Mistake
    return partial_area > total_area / 2 - full_area
    Fix
    return partial_area >= total_area / 2 - full_area

    What happens: In the example, the total area is 2, so half is 1. Every line from y = 1 to y = 2 has exactly area 1 below it. With >=, the check is True on those lines, so hi = mid moves down and the search returns the lowest line, 1.0. With >, the check is False on those lines, because 1 is not greater than 1. So lo = mid moves up, and the first True line is just above 2. The call returns 2.0000000000000004, and the problem asks for the lowest line.

    Example: Separate Squares I (LC 3453): squares = [[0, 0, 1], [2, 2, 1]]
    Mistake: returns 2.0000000000000004Fix: returns 1.0
Not this one if…
  • The answer must be a whole number, the piece length in Cutting Ribbons (LC 1891): every candidate has a next one, so the integer loop ends exactly on the answer, with no tolerance or round count to reason about. See Feasible Function
  • The values being ranked are whole numbers, the pair distances in Find K-th Smallest Pair Distance (LC 719): the k-th smallest is an integer the value search lands on exactly with lo = mid + 1, and the answer is an integer, so the integer loop ends on it. See Kth Smallest

Theory

The question we build

What we know

The area below a horizontal line never gets smaller when the line rises. So the question "is at least half of the area below this line?" is False for low lines and True from some line on.

What the problem gives us

LeetCode accepts an answer within 10^-5. So the search needs a final range narrower than 10^-5, not the exact value.

The question we build

We use squares = [[0, 0, 2], [1, 1, 1]]. The total area is 4 + 1 = 5, so half of it is 2.5. The lowest bottom edge is 0 and the highest top edge is 2, so we try the lines y = 0, 1, 2. The area below them is 0, 2, 5, so the answers are False, False, True. The boundary is between y = 1 and y = 2. The exact answer is 7/6 = 1.16667.

The check at_least_half_below(y) for each candidate: 0 is False, 1 is False, 2 is True.
y
0
1
2
at_least_half_below(y)
F
F
T

The sample lines are whole numbers. The boundary is a real number between y = 1 and y = 2.

Narrowing down to the template

The first True line is the answer, so the call is binary_search_lower_bound_reals(lo, hi, at_least_half_below). At lo, the lowest bottom edge, no area is below the line, so the check is False. At hi, the highest top edge, all the area is below the line, so the check is True.

Step 1: Try values on a grid

The answer is the first value where the check is True. A real number has no next value to step to, so a scan can only try values on a grid.

Step 1: Try values on a gridPython
1def first_true_scan_reals(lo, hi, is_feasible, step=0.001):
2 y = lo
3 while y < hi and not is_feasible(y):
4 y += step
5 return y

Cost: O((hi - lo) / step) checks, and the answer is correct only to step. For a boundary at 0.3 on [0, 1] it takes about 300 checks.

Step 2: Halve the range for a fixed number of rounds

Halving keeps the first True value between lo and hi, as in Lower Bound. The table lists the five changes that real numbers need.

Step 2: Halve the range for a fixed number of roundsPython
1def binary_search_lower_bound_reals(lo: float, hi: float, is_feasible: Callable[[float], bool], rounds: int = 100) -> float:
2 """First True boundary in [lo, hi], approximated up to floating-point precision.
3 In exact arithmetic, width is initial_width / 2**rounds; float rounding limits further shrinkage.
4 Precondition: is_feasible is monotonic (False, False, ..., True, True).
5 Returns hi when no value is True. T = the cost of one is_feasible call.
6 Time: O(T * rounds) | Space: O(1)
7 """
8 for _ in range(rounds):
9 mid = (lo + hi) / 2
10 # 1. Check the Midpoint: is_feasible(mid) shows which side of the boundary mid is on.
11 if is_feasible(mid):
12 hi = mid # 2. Narrow Toward Mid from Above: mid works, so move hi down to mid.
13 else:
14 lo = mid # 3. Narrow Toward Mid from Below: mid fails, so move lo up to mid.
15 
16 # 4. Return the Approximation: after rounds halvings, hi is on the True side within float precision.
17 return hi

Cost: O(T * rounds), where T is the cost of one call of is_feasible.

ItemLower Bound (whole numbers)Continuous Bisection
Loopfor _ in range(rounds):A fixed number of rounds replaces while lo < hi.
mid = lo + (hi - lo) // 2mid = (lo + hi) / 2True division: mid is a real number.
When the check is True: hi = midhi = midUnchanged.
When the check is False: lo = mid + 1lo = midThere is no next value to step to.
Result: lo, the exact first Truehi, within a tiny error of the first Truehi is the True end, or the starting hi when nothing is True.

How many rounds

A float is the number type Python uses for decimals. It keeps about 16 significant digits. To reach an error eps on a range of width hi - lo, you need about log2((hi - lo) / eps) rounds. For squares = [[0, 0, 2], [1, 1, 1]] the width is 2 and eps is 10^-5, so about 18 rounds are enough. The call with rounds = 18 returns 1.1666717529296875, which is 5.1e-6 from 7/6. The default of 100 rounds is far more than needed. The extra rounds only cost time, because float rounding stops the width from shrinking.

Stopping by width

A loop while hi - lo > eps also works when eps is larger than the gap between two floats at the size of the values. For eps = 1e-9 on a range of width 10, it takes about 34 rounds. Near 109, two neighbouring floats are about 1.2e-7 apart, so hi - lo never gets below 1e-9 and the loop never ends. A round count always ends.

The largest value that passes

The card returns hi, the end that passes the check. To find the largest value that passes, give the failing end as lo and the passing end as hi, even though lo is then the larger number. The loop does not change. For the check x * x <= 2, the call binary_search_lower_bound_reals(2, 0, check) returns about 1.414, the square root of 2.

Compare the answer with a tolerance

The result is a float close to the exact value, and it is not always the same float. Compare with abs(answer - expected) <= 1e-5, as LeetCode does, not with ==. For squares = [[0, 0, 2], [0, 0, 1]] the exact answer is 5/6. The search returns 0.8333333333333333, and 5 / 6 is 0.8333333333333334, so == is False.

Real-world use

Numerical libraries find the root of a monotonic function by bisection. They start with one point where the function is below zero and one point where it is above zero, and they halve the range between the two points. The first point plays the role of lo, which is False, and the second point plays the role of hi, which is True.

Say it in an interview

"The answer is a real number, so I use Lower Bound over real numbers. The area below a horizontal line only grows as the line rises, so "is at least half of the area below this line?" is False and then True. I start lo at the lowest bottom edge and hi at the highest top edge. At mid, a passing check sets hi = mid and a failing check sets lo = mid, because a real number has no next value. I run a fixed 100 rounds instead of while lo < hi, because neighbouring floats make that loop never end, and I return hi. Each round costs one pass over the N squares, so the time is O(N * rounds) and the space is O(1)."

Common Questions

What if the answer is an exact fraction, as in K-th Smallest Prime Fraction (LC 786)?

hi is a float close to the fraction, not the fraction itself. After the rounds, make one more pass that finds the largest fraction that is at most hi, and compare two fractions with a * d > c * b instead of a / b > c / d. This is exact because the final range is narrower than the smallest gap between two different fractions, which is at least 1 / (3 * 104)^2 there.

Practice

Canonical LeetCode:Separate Squares I (LC 3453)Minimize Max Distance to Gas Station (LC 774)Maximum Average Subarray II (LC 644)Pour Water Between Buckets to Make Water Levels Equal (LC 2137)K-th Smallest Prime Fraction (LC 786)

The older version of this card had a general loop named bisect_reals, and an anchor with the check written inside the loop. This course names the loop binary_search_lower_bound_reals, like the other cards, and writes the check as its own function at_least_half_below.

The classic loop

Old Way
Python
1from typing import Callable, List
2 
3def bisect_reals(lo: float, hi: float, is_feasible: Callable[[float], bool], rounds: int = 100) -> float:
4 """Continuous Bisection: the real number where is_feasible flips, to within float precision.
5 Precondition: lo fails the check, hi passes it, and the check flips only once between them.
6 lo and hi themselves are never checked. For the highest value that passes, give the failing end as lo.
7 R = rounds. T = the cost of one is_feasible call.
8 Time: O(R * T) | Space: O(1)
9 """
10 for _ in range(rounds): # Fixed rounds: a real number has no next value
11 mid = (lo + hi) / 2 # True division: mid is a real number
12 if is_feasible(mid):
13 hi = mid # mid passes: the answer is mid, or closer to lo
14 else:
15 lo = mid # mid fails: move onto mid, never mid + 1
16 
17 return hi # hi always passes: the boundary, far inside 10^-5
18 
19 
20# Separate Squares I (LC 3453): the template above, with its check written inside the loop (the step debugger runs this code).
21def separateSquares(squares: List[List[int]]) -> float:
22 """Separate Squares I: bisect the height y = mid until the window is far narrower than 10^-5.
23 R = 100 rounds, each one pass over the N squares to measure the area below mid.
24 Time: O(N * R) | Space: O(1)
25 """
26 total = sum(l * l for _, _, l in squares) # Area of every square; overlaps count once per square
27 lo = min(y for _, y, _ in squares) # No area is below lo: the check fails here
28 hi = max(y + l for _, y, l in squares) # All area is below hi: the check passes here
29 
30 for _ in range(100): # Fixed rounds: a real number has no next value
31 mid = (lo + hi) / 2 # True division: mid is a real number
32 full = sum(l * l for _, y, l in squares if y + l <= mid) # Squares wholly below mid: an exact integer
33 part = sum(l * (mid - y) for _, y, l in squares if y < mid < y + l) # Squares mid cuts: the only float part
34 if 2 * part >= total - 2 * full: # >=, not >: keeps the lowest balancing line
35 hi = mid # Half the area is below: the answer is mid or lower
36 else:
37 lo = mid # Too low: the answer is above mid (never mid + 1)
38 
39 return hi # The lowest balancing line, well inside 10^-5

From the classic loop to our template

Old Way lineIn our templateWhy
def bisect_reals(lo, hi, is_feasible, rounds=100)def binary_search_lower_bound_reals(lo, hi, is_feasible, rounds=100)The same loop. The new name follows the other cards.
the check inside the loop of separateSquaresdef at_least_half_below(line_y)The same check, in its own function, so the template can call it.
full = sum(...) and part = sum(...)full_area and partial_area, with plain loopsThe same exact whole number and the same decimal. A plain loop shows every step.
if 2 * part >= total - 2 * full:return partial_area >= total_area / 2 - full_areaThe same comparison, moved into the check.

When the classic form is fine

Use it when an interviewer asks for this form, or when the code base already uses it. It is the same search. The template only gives the loop a name and moves the check into a function.

Gotchas of the classic loop

Gotchas1 of 7

Gotcha 1 of 7

  1. 1
    The trap

    Gotcha 1: Adding the whole squares and the cut squares into one float

    Mistake
    total_lower = sum(l * l for _, y, l in squares if y + l <= mid)
    total_lower += sum(l * (mid - y) for _, y, l in squares if y < mid < y + l)
    if 2 * total_lower >= total:
    Fix
    full = sum(l * l for _, y, l in squares if y + l <= mid)
    part = sum(l * (mid - y) for _, y, l in squares if y < mid < y + l)
    if 2 * part >= total - 2 * full:

    What happens: A float keeps only about 16 significant digits. The whole squares can add up to a huge area, around 1011 here. At that size, the smallest step a float can hold is about 0.00006. When the small slice of a cut square, like 0.99997, is added to that sum, it is rounded to 1.0. The check then passes for a mid that is slightly too low. So hi = mid moves the window below the true answer. The loop is correct. The check is wrong. The fix keeps the two kinds of area apart. full holds the squares wholly below mid, and it is an exact integer. part holds only the squares that mid cuts, so it stays small. total - 2 * full is integer arithmetic, so the huge numbers cancel exactly. The float 2 * part is then compared against a small exact number. In the example, the check becomes 2 * part >= 2, which flips exactly at mid = 1000001. A quick test before coding: multiply the largest value in a float sum by 10^-16. The result must be far smaller than the tolerance. Here 1011 * 10^-16 = 10^-5, which is the tolerance itself, so there is no margin.

    Example: Separate Squares I (LC 3453): squares = [[0, 1000000, 1], [0, 0, 600000], [0, 100000000, 600000], [0, 1000001, 1]]
    Mistake: returns 1000000.9999694824Fix: returns 1000001.0
  2. 2

    Gotcha 2: Looping while lo < hi on floats

    Mistake
    while lo < hi:
    Fix
    for _ in range(100):

    What happens: Between two floats there is only a limited number of other floats. After enough rounds, lo and hi are neighbouring floats. Then mid = (lo + hi) / 2 equals lo or hi. In the example, mid equals hi and the check passes. So hi = mid changes nothing, and the loop never ends. for _ in range(100) stops after 100 rounds.

    Example: Separate Squares I (LC 3453): squares = [[0, 0, 1], [2, 2, 1]]
    Mistake: the loop never endsFix: returns 1.0
  3. 3

    Gotcha 3: Stopping on hi - lo > eps with a tiny eps

    Mistake
    while hi - lo > 1e-9:
    Fix
    for _ in range(100):

    What happens: Near 109, two neighbouring floats are about 1.2e-7 apart. So hi - lo can never drop below 1e-9 there, and the loop never ends. A fixed number of rounds always ends. Each round halves the window, so t rounds leave (hi - lo) / 2^t. A window of 2 * 109 needs 48 rounds to get below 10^-5, so 100 rounds is a safe default.

    Example: squares = [[0, 1000000000, 2]]
    Mistake: the loop never endsFix: returns 1000000001.0
  4. 4

    Gotcha 4: Writing > instead of >= in the area check

    Mistake
    if 2 * part > total - 2 * full:
    Fix
    if 2 * part >= total - 2 * full:

    What happens: Sometimes a whole stretch of lines splits the area in half. In the example, every line from y = 1 to y = 2 does. With >=, a balancing mid moves hi down, so the search keeps the lowest line, 1. With >, a balancing mid moves lo up, so it returns the highest line, 2.

    Example: squares = [[0, 0, 1], [2, 2, 1]]
    Mistake: returns 2.0000000000000004Fix: returns 1.0
  5. 5

    Gotcha 5: Not knowing what lo and hi hold at the end

    Mistake
    return lo
    Fix
    return hi

    What happens: The invariant tells you what each end means. lo always fails the check, and hi always passes it. After 100 rounds they are neighbouring floats, so the answer sits between them. hi is the lowest value known to pass. lo is the highest value known to fail. For "find the smallest h that balances", return hi. Both ends are within 10^-5 of the answer, so lo is usually accepted too. But lo is the failing side, and it lands slightly below the answer. return mid is worse. mid is only the last guess, and it may be on either side. If the problem asks for the highest value that passes, the roles swap. The passing end is then the lower number, and you return that one.

    Example: Separate Squares I (LC 3453): squares = [[0, 0, 1], [2, 2, 1]]
    Mistake: returns 0.9999999999999999Fix: returns 1.0
  6. 6

    Gotcha 6: Returning hi when the answer is an exact fraction

    Mistake
    return hi
    Fix
    best = [0, 1]
    i = 0
    for j in range(1, n):
        while i < j and arr[i] <= hi * arr[j]:
            i += 1
        if i > 0 and arr[i - 1] * best[1] > best[0] * arr[j]:
            best = [arr[i - 1], arr[j]]
    return best

    What happens: return hi gives a float close to the answer, not the answer itself. K-th Smallest Prime Fraction (LC 786) asks for the fraction as [numerator, denominator]. The standard way to get it is one more pass after the rounds. It finds the largest fraction that is at most hi. best = [0, 1] starts at the fraction 0 / 1, which is smaller than every fraction in the input. For each denominator arr[j], i moves forward while arr[i] <= hi * arr[j]. Then arr[i - 1] / arr[j] is the largest fraction with denominator arr[j] that is at most hi. It replaces best when it is larger, and the comparison arr[i - 1] * best[1] > best[0] * arr[j] multiplies integers, so it is exact. The pass is safe because the final window is narrower than the smallest gap between two different fractions. There that gap is at least 1 / (3 * 104)^2.

    Example: K-th Smallest Prime Fraction (LC 786): arr = [1, 2, 3, 5], k = 3
    Mistake: returns 0.4Fix: returns [2, 5]
  7. 7

    Gotcha 7: Comparing two fractions with / instead of *

    Mistake
    if arr[i - 1] / arr[j] > best[0] / best[1]:
    Fix
    if arr[i - 1] * best[1] > best[0] * arr[j]:

    What happens: a / b > c / d rounds each fraction to a float before it compares them. A float keeps only about 16 significant digits. So two fractions that differ by less than that become the same float, or swap order. a * d > c * b asks the same question, because b and d are positive. It multiplies integers, and Python integers are exact at any size. So the answer is always right. In K-th Smallest Prime Fraction (LC 786), two different fractions differ by at least 1 / (3 * 104)^2, so / happens to work there. * needs no such check. Also * never divides by zero. The same rule does not make arr[i] <= mid * arr[j] exact when mid is a float. There mid * arr[j] is rounded once, just like arr[i] / arr[j]. That is why the last step compares the fractions as integers.

    Example: a / b = (10^16 + 1) / 10^16 against c / d = 1 / 1
    Mistake: returns FalseFix: returns True

Earlier signals

The answer is a real number, not an integer or an index. It can be a height, a distance, a water level or an average, which can fall between any two values. The problem accepts the answer within a tolerance such as 10^-5. An answer counts as correct when it differs from the true value by at most 0.00001. The answer can also be a fraction you must rank. A guess is easy to check, and the check flips only once as the guess grows.

Earlier invariant

lo always fails the check and hi always passes it, so the answer is always between them, and each round halves that window.

Earlier direction

real-valued answer (accepted within a tolerance such as 10^-5 = 0.00001, or a ratio or fraction) âž” move lo or hi onto mid for a fixed number of rounds âž” answer within the tolerance

Earlier description

A real number has no next value after mid. So lo = mid + 1 and hi = mid - 1 have no value to step to. Instead, move one end onto mid itself. Set hi = mid when is_feasible(mid) passes, and set lo = mid when it fails. In the code for Separate Squares I (LC 3453), the check is 2 * part >= total - 2 * full. It passes when at least half of the total area is below the line y = mid. lo always fails the check and hi always passes it. So the answer never leaves the window between lo and hi. Each round halves that window. So a fixed number of rounds, for _ in range(100), makes the window far narrower than 10^-5. 10^-5 is 0.00001, the largest error the problem accepts. The same loop also finds the highest value that passes. For that, give the failing end as lo, even though it is the larger number. The trap is the integer loop while lo < hi, which never ends on floats. After enough rounds, lo and hi are neighbouring floats. Then mid equals lo or hi, so hi = mid or lo = mid changes nothing.

Earlier look-alikes

The values being ranked are whole numbers, the pair distances in Find K-th Smallest Pair Distance (LC 719): the k-th smallest is an integer the value search lands on exactly with lo = mid + 1, and there is nothing to snap.

Earlier template

Earlier template
Python
1from typing import Callable, List
2 
3def bisect_reals(lo: float, hi: float, is_feasible: Callable[[float], bool], rounds: int = 100) -> float:
4 """Continuous Bisection: the real number where is_feasible flips, to within float precision.
5 Precondition: lo fails the check, hi passes it, and the check flips only once between them.
6 lo and hi themselves are never checked. For the highest value that passes, give the failing end as lo.
7 R = rounds. T = the cost of one is_feasible call.
8 Time: O(R * T) | Space: O(1)
9 """
10 for _ in range(rounds): # Fixed rounds: a real number has no next value
11 mid = (lo + hi) / 2 # True division: mid is a real number
12 if is_feasible(mid):
13 hi = mid # mid passes: the answer is mid, or closer to lo
14 else:
15 lo = mid # mid fails: move onto mid, never mid + 1
16 
17 return hi # hi always passes: the boundary, far inside 10^-5
18 
19 
20# Separate Squares I (LC 3453): the template above, with its check written inside the loop (the step debugger runs this code).
21def separateSquares(squares: List[List[int]]) -> float:
22 """Separate Squares I: bisect the height y = mid until the window is far narrower than 10^-5.
23 R = 100 rounds, each one pass over the N squares to measure the area below mid.
24 Time: O(N * R) | Space: O(1)
25 """
26 total = sum(l * l for _, _, l in squares) # Area of every square; overlaps count once per square
27 lo = min(y for _, y, _ in squares) # No area is below lo: the check fails here
28 hi = max(y + l for _, y, l in squares) # All area is below hi: the check passes here
29 
30 for _ in range(100): # Fixed rounds: a real number has no next value
31 mid = (lo + hi) / 2 # True division: mid is a real number
32 full = sum(l * l for _, y, l in squares if y + l <= mid) # Squares wholly below mid: an exact integer
33 part = sum(l * (mid - y) for _, y, l in squares if y < mid < y + l) # Squares mid cuts: the only float part
34 if 2 * part >= total - 2 * full: # >=, not >: keeps the lowest balancing line
35 hi = mid # Half the area is below: the answer is mid or lower
36 else:
37 lo = mid # Too low: the answer is above mid (never mid + 1)
38 
39 return hi # The lowest balancing line, well inside 10^-5

Earlier gotchas

Adding the whole squares and the cut squares into one float

A float keeps only about 16 significant digits. The whole squares can add up to a huge area, around 1011 here. At that size, the smallest step a float can hold is about 0.00006. When the small slice of a cut square, like 0.99997, is added to that sum, it is rounded to 1.0. The check then passes for a mid that is slightly too low. So hi = mid moves the window below the true answer. The loop is correct. The check is wrong. The fix keeps the two kinds of area apart. full holds the squares wholly below mid, and it is an exact integer. part holds only the squares that mid cuts, so it stays small. total - 2 * full is integer arithmetic, so the huge numbers cancel exactly. The float 2 * part is then compared against a small exact number. In the example, the check becomes 2 * part >= 2, which flips exactly at mid = 1000001. A quick test before coding: multiply the largest value in a float sum by 10^-16. The result must be far smaller than the tolerance. Here 1011 * 10^-16 = 10^-5, which is the tolerance itself, so there is no margin.

Looping while lo < hi on floats

Between two floats there is only a limited number of other floats. After enough rounds, lo and hi are neighbouring floats. Then mid = (lo + hi) / 2 equals lo or hi. In the example, mid equals hi and the check passes. So hi = mid changes nothing, and the loop never ends. for _ in range(100) stops after 100 rounds.

Stopping on hi - lo > eps with a tiny eps

Near 109, two neighbouring floats are about 1.2e-7 apart. So hi - lo can never drop below 1e-9 there, and the loop never ends. A fixed number of rounds always ends. Each round halves the window, so t rounds leave (hi - lo) / 2^t. A window of 2 * 109 needs 48 rounds to get below 10^-5, so 100 rounds is a safe default.

Writing > instead of >= in the area check

Sometimes a whole stretch of lines splits the area in half. In the example, every line from y = 1 to y = 2 does. With >=, a balancing mid moves hi down, so the search keeps the lowest line, 1. With >, a balancing mid moves lo up, so it returns the highest line, 2.

Not knowing what lo and hi hold at the end

The invariant tells you what each end means. lo always fails the check, and hi always passes it. After 100 rounds they are neighbouring floats, so the answer sits between them. hi is the lowest value known to pass. lo is the highest value known to fail. For "find the smallest h that balances", return hi. Both ends are within 10^-5 of the answer, so lo is usually accepted too. But lo is the failing side, and it lands slightly below the answer. return mid is worse. mid is only the last guess, and it may be on either side. If the problem asks for the highest value that passes, the roles swap. The passing end is then the lower number, and you return that one.

Returning hi when the answer is an exact fraction

return hi gives a float close to the answer, not the answer itself. K-th Smallest Prime Fraction (LC 786) asks for the fraction as [numerator, denominator]. The standard way to get it is one more pass after the rounds. It finds the largest fraction that is at most hi. best = [0, 1] starts at the fraction 0 / 1, which is smaller than every fraction in the input. For each denominator arr[j], i moves forward while arr[i] <= hi * arr[j]. Then arr[i - 1] / arr[j] is the largest fraction with denominator arr[j] that is at most hi. It replaces best when it is larger, and the comparison arr[i - 1] * best[1] > best[0] * arr[j] multiplies integers, so it is exact. The pass is safe because the final window is narrower than the smallest gap between two different fractions. There that gap is at least 1 / (3 * 104)^2.

Comparing two fractions with / instead of *

a / b > c / d rounds each fraction to a float before it compares them. A float keeps only about 16 significant digits. So two fractions that differ by less than that become the same float, or swap order. a * d > c * b asks the same question, because b and d are positive. It multiplies integers, and Python integers are exact at any size. So the answer is always right. In K-th Smallest Prime Fraction (LC 786), two different fractions differ by at least 1 / (3 * 104)^2, so / happens to work there. * needs no such check. Also * never divides by zero. The same rule does not make arr[i] <= mid * arr[j] exact when mid is a float. There mid * arr[j] is rounded once, just like arr[i] / arr[j]. That is why the last step compares the fractions as integers.

© 2026 Hi👋WebEnterprise. All rights reserved.
Sitemap•llms.txt•