Builds on Feasible Function and Lower Bound, which you haven’t finished yet.
Binary Search on Real Numbers (Bisection Method)
lo is equal to hi.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.
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.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 tolerance10^-5. A yes-or-no question about one guess is False for small guesses and True from some value on.| 1 | from typing import Callable, List |
| 2 | Â |
| 3 | Â |
| 4 | def 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 |
(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 / 8lo 0hi 2mid none
Each column is one round of the loop. The last column holds the final lo and hi.
lothe end of the range that fails the checkhithe end of the range that passes the checkmidthe position the check reads nowcheckTrue when at least half of the area is below midanswerthe 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.
Gotcha 1 of 7
- The trap
Gotcha 1: Looping
while lo < hion floatsMistakewhile lo < hi:Fixfor _ in range(rounds):What happens: Between two floats there is only a limited number of other floats. After enough rounds,
loandhiare neighbouring floats. In the example, this happens after 54 rounds, withlo = 0.9999999999999999andhi = 1.0. Thenmid = (lo + hi) / 2equalshi, the check passes, andhi = midchanges nothing. Solo < histays 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
- 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 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.
| 1 | def 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.
| 1 | def 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.
| Item | Lower Bound (whole numbers) | Continuous Bisection |
|---|---|---|
| Loop | for _ in range(rounds): | A fixed number of rounds replaces while lo < hi. |
mid = lo + (hi - lo) // 2 | mid = (lo + hi) / 2 | True division: mid is a real number. |
When the check is True: hi = mid | hi = mid | Unchanged. |
When the check is False: lo = mid + 1 | lo = mid | There is no next value to step to. |
Result: lo, the exact first True | hi, within a tiny error of the first True | hi 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
loat the lowest bottom edge andhiat the highest top edge. Atmid, a passing check setshi = midand a failing check setslo = mid, because a real number has no next value. I run a fixed 100 rounds instead ofwhile lo < hi, because neighbouring floats make that loop never end, and I returnhi. Each round costs one pass over theNsquares, so the time isO(N * rounds)and the space isO(1)."
Common Questions
- What if the answer is an exact fraction, as in K-th Smallest Prime Fraction (LC 786)?
hiis 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 mosthi, and compare two fractions witha * d > c * binstead ofa / b > c / d. This is exact because the final range is narrower than the smallest gap between two different fractions, which is at least1 / (3 * 104)^2there.
Practice
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
| 1 | from typing import Callable, List |
| 2 | Â |
| 3 | def 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). |
| 21 | def 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 line | In our template | Why |
|---|---|---|
| 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 separateSquares | def 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 loops | The 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_area | The 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
Gotcha 1 of 7
- The trap
Gotcha 1: Adding the whole squares and the cut squares into one float
Mistaketotal_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:Fixfull = 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
1011here. At that size, the smallest step a float can hold is about0.00006. When the small slice of a cut square, like0.99997, is added to that sum, it is rounded to1.0. The check then passes for amidthat is slightly too low. Sohi = midmoves the window below the true answer. The loop is correct. The check is wrong. The fix keeps the two kinds of area apart.fullholds the squares wholly belowmid, and it is an exact integer.partholds only the squares thatmidcuts, so it stays small.total - 2 * fullis integer arithmetic, so the huge numbers cancel exactly. The float2 * partis then compared against a small exact number. In the example, the check becomes2 * part >= 2, which flips exactly atmid = 1000001. A quick test before coding: multiply the largest value in a float sum by10^-16. The result must be far smaller than the tolerance. Here1011 * 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
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
| 1 | from typing import Callable, List |
| 2 | Â |
| 3 | def 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). |
| 21 | def 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.
Builds on Feasible Function and Lower Bound, which you haven’t finished yet.
Binary Search on Real Numbers (Bisection Method)
lo is equal to hi.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.
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.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 tolerance10^-5. A yes-or-no question about one guess is False for small guesses and True from some value on.| 1 | from typing import Callable, List |
| 2 | Â |
| 3 | Â |
| 4 | def 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 |
(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 / 8lo 0hi 2mid none
Each column is one round of the loop. The last column holds the final lo and hi.
lothe end of the range that fails the checkhithe end of the range that passes the checkmidthe position the check reads nowcheckTrue when at least half of the area is below midanswerthe 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.
Gotcha 1 of 7
- The trap
Gotcha 1: Looping
while lo < hion floatsMistakewhile lo < hi:Fixfor _ in range(rounds):What happens: Between two floats there is only a limited number of other floats. After enough rounds,
loandhiare neighbouring floats. In the example, this happens after 54 rounds, withlo = 0.9999999999999999andhi = 1.0. Thenmid = (lo + hi) / 2equalshi, the check passes, andhi = midchanges nothing. Solo < histays 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
- 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 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.
| 1 | def 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.
| 1 | def 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.
| Item | Lower Bound (whole numbers) | Continuous Bisection |
|---|---|---|
| Loop | for _ in range(rounds): | A fixed number of rounds replaces while lo < hi. |
mid = lo + (hi - lo) // 2 | mid = (lo + hi) / 2 | True division: mid is a real number. |
When the check is True: hi = mid | hi = mid | Unchanged. |
When the check is False: lo = mid + 1 | lo = mid | There is no next value to step to. |
Result: lo, the exact first True | hi, within a tiny error of the first True | hi 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
loat the lowest bottom edge andhiat the highest top edge. Atmid, a passing check setshi = midand a failing check setslo = mid, because a real number has no next value. I run a fixed 100 rounds instead ofwhile lo < hi, because neighbouring floats make that loop never end, and I returnhi. Each round costs one pass over theNsquares, so the time isO(N * rounds)and the space isO(1)."
Common Questions
- What if the answer is an exact fraction, as in K-th Smallest Prime Fraction (LC 786)?
hiis 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 mosthi, and compare two fractions witha * d > c * binstead ofa / b > c / d. This is exact because the final range is narrower than the smallest gap between two different fractions, which is at least1 / (3 * 104)^2there.
Practice
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
| 1 | from typing import Callable, List |
| 2 | Â |
| 3 | def 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). |
| 21 | def 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 line | In our template | Why |
|---|---|---|
| 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 separateSquares | def 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 loops | The 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_area | The 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
Gotcha 1 of 7
- The trap
Gotcha 1: Adding the whole squares and the cut squares into one float
Mistaketotal_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:Fixfull = 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
1011here. At that size, the smallest step a float can hold is about0.00006. When the small slice of a cut square, like0.99997, is added to that sum, it is rounded to1.0. The check then passes for amidthat is slightly too low. Sohi = midmoves the window below the true answer. The loop is correct. The check is wrong. The fix keeps the two kinds of area apart.fullholds the squares wholly belowmid, and it is an exact integer.partholds only the squares thatmidcuts, so it stays small.total - 2 * fullis integer arithmetic, so the huge numbers cancel exactly. The float2 * partis then compared against a small exact number. In the example, the check becomes2 * part >= 2, which flips exactly atmid = 1000001. A quick test before coding: multiply the largest value in a float sum by10^-16. The result must be far smaller than the tolerance. Here1011 * 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
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
| 1 | from typing import Callable, List |
| 2 | Â |
| 3 | def 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). |
| 21 | def 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.