3453. Separate Squares I
Problem Description
You are given a 2D integer array squares. Each squares[i] = [xi, yi, li] represents the coordinates of the bottom-left point and the side length of a square that is parallel to the x-axis.
Your task is to find the minimum y-coordinate value of a horizontal line such that the total area of the squares above the line is equal to the total area of the squares below the line.
A few important details to keep in mind:
- The horizontal line is described by a single value
y. Everything with a y-coordinate greater thanyis considered "above" the line, and everything with a y-coordinate less thanyis considered "below" the line. When the line cuts through a square, that square contributes part of its area above the line and part of its area below. - The squares may overlap with one another. When they do, the overlapping regions should be counted multiple times (once for each square that covers that region).
- Answers within
10^-5of the actual answer will be accepted, so a small numerical tolerance is allowed.
In short, you need to determine the lowest possible y-coordinate where a horizontal line splits the combined square areas into two equal halves.
How We Pick the Algorithm
Why Binary Search?
This problem maps to Binary Search through a short path in the full flowchart.
The area below a horizontal line increases monotonically as the line moves up, so binary search on the answer finds the split point.
Open in FlowchartIntuition
The key observation is to think about how the area below the horizontal line changes as we move the line up and down.
Imagine we start with the line at the very bottom (y = 0). At this position, no square area is below the line, so the area below is 0. Now, as we slowly raise the line upward, more and more of each square falls below it, so the area below keeps increasing. Eventually, when the line reaches the top of every square, all the area is below the line and the area above becomes 0.
This tells us something very useful: the area below the line is a monotonically increasing function of the line's y-coordinate. As y goes up, the area below never decreases, and the area above never increases. Whenever a quantity changes monotonically and we want to find the exact point where it hits a target value, this is a classic signal to use binary search.
So what is our target? We want the area below the line to equal the area above the line. Since these two amounts must add up to the total area of all squares, having them be equal simply means the area below the line should be exactly half of the total area. We compute the total area once as s = sum(li * li), and our goal becomes finding the smallest y where the area below is at least s / 2.
Now we set up the binary search range. The lowest the line could ever need to be is y = 0, so that is our left boundary l. The highest meaningful position is the very top of the tallest square, which is max(yi + li), so that becomes our right boundary r. Beyond this point, raising the line adds nothing new.
For any candidate line at height mid, we calculate the area below it by checking each square: if a square's bottom y is below mid, the portion of that square lying under the line has height min(mid - y, l) (capped at the full side length l once the line passes over the whole square), and multiplying by the side length l gives that square's contribution. Summing these up gives the total area below. The overlap rule is naturally handled here, because we add each square's contribution independently without subtracting shared regions.
Finally, we compare this area against s / 2. If the area below is already at least half, the correct answer is at this height or lower, so we move r down to mid. Otherwise we need more area below, so we push l up to mid. We keep shrinking the interval until r - l is smaller than a tiny tolerance like 10^-5, at which point either boundary is a valid answer within the allowed precision.
Pattern Learn more about Binary Search patterns.
Solution Approach
Solution 1: Binary Search
According to the problem, we need to find a horizontal line such that the total area of squares above the line equals the total area of squares below the line. Since the area below the line increases as the y-coordinate increases (and the area above decreases correspondingly), this monotonic property allows us to use binary search to locate the y-coordinate of the line.
Step 1: Compute the total area.
We first calculate the total area of all squares as s = sum(a[2] * a[2] for a in squares), where a[2] is the side length l of each square. Since the area above and the area below the line must add up to this total, our goal reduces to finding the smallest y where the area below the line is at least s / 2.
Step 2: Set up the search boundaries.
We define the left boundary of the binary search as l = 0, since the line never needs to go below the bottom. The right boundary is r = max(a[1] + a[2] for a in squares), which represents the highest top edge among all squares (y + l for each square). Beyond this point, raising the line gains no additional area.
Step 3: Define the check function.
For a candidate line height y1, the function check(y1) computes the area lying below the line:
- For each square
(x, y, l), if its bottom edgeyis belowy1, then part (or all) of it lies under the line. - The height of the portion below the line is
min(y1 - y, l). Wheny1 - yexceedsl, the entire square is below the line, so the height is capped atl. - Multiplying this height by the side length
lgives that square's contributionl * min(y1 - y, l), which we accumulate intot.
The overlap requirement is handled automatically here, because each square's contribution is added independently without subtracting any shared regions. The function returns whether t >= s / 2.
Step 4: Run the binary search.
We repeatedly compute the midpoint mid = (l + r) / 2:
- If
check(mid)isTrue, the area below is already at least half, so the answer is at this height or lower. We move the right boundary down withr = mid. - Otherwise, we need more area below, so we move the left boundary up with
l = mid.
We continue this loop while r - l > eps, where eps = 1e-5 ensures the result is within the accepted precision. Once the interval is small enough, we return r as the answer.
Complexity Analysis.
The time complexity is O(n * log(M / eps)), where n is the number of squares and M is the value of the upper boundary r. Each binary search iteration runs the check function in O(n) time, and the number of iterations depends on the ratio of the search range to the precision eps. The space complexity is O(1), since only a constant amount of extra space is used.
Example Walkthrough
Let's trace through the solution with a small, concrete example.
Input: squares = [[0, 0, 2], [1, 1, 2]]
We have two squares:
- Square A: bottom-left at
(0, 0)with side length2, so it spansyfrom0to2. - Square B: bottom-left at
(1, 1)with side length2, so it spansyfrom1to3. (Notice it overlaps with Square A in the region1 ≤ y ≤ 2.)
Step 1: Compute the total area.
s = (2 * 2) + (2 * 2) = 4 + 4 = 8
Our target is to find the smallest y where the area below is at least s / 2 = 4.
Step 2: Set up the search boundaries.
l = 0 r = max(0 + 2, 1 + 2) = max(2, 3) = 3
So the line lives somewhere in the interval [0, 3].
Step 3 & 4: Run the binary search using check(y1).
Recall check(y1) sums l * min(y1 - y, l) for each square whose bottom y is below y1, and returns whether that sum is ≥ 4.
Iteration 1: mid = (0 + 3) / 2 = 1.5
- Square A:
min(1.5 - 0, 2) = 1.5, contributes2 * 1.5 = 3.0 - Square B:
min(1.5 - 1, 2) = 0.5, contributes2 * 0.5 = 1.0 - Area below
= 3.0 + 1.0 = 4.0, which is≥ 4→True - Answer is at this height or lower, so
r = 1.5. Interval:[0, 1.5]
Iteration 2: mid = (0 + 1.5) / 2 = 0.75
- Square A:
min(0.75 - 0, 2) = 0.75, contributes2 * 0.75 = 1.5 - Square B: bottom
y = 1is not below0.75, so it contributes0 - Area below
= 1.5, which is< 4→False - Need more area, so
l = 0.75. Interval:[0.75, 1.5]
Iteration 3: mid = (0.75 + 1.5) / 2 = 1.125
- Square A:
min(1.125 - 0, 2) = 1.125, contributes2 * 1.125 = 2.25 - Square B:
min(1.125 - 1, 2) = 0.125, contributes2 * 0.125 = 0.25 - Area below
= 2.25 + 0.25 = 2.5, which is< 4→False l = 1.125. Interval:[1.125, 1.5]
Iteration 4: mid = (1.125 + 1.5) / 2 = 1.3125
- Square A:
min(1.3125, 2) = 1.3125, contributes2.625 - Square B:
min(0.3125, 2) = 0.3125, contributes0.625 - Area below
= 3.25, which is< 4→False l = 1.3125. Interval:[1.3125, 1.5]
The interval keeps shrinking, converging toward y = 1.5. We can verify y = 1.5 is exactly correct:
- Below the line:
3.0 (from A) + 1.0 (from B) = 4.0 - Above the line: A contributes
2 * (2 - 1.5) = 1.0, B contributes2 * (3 - 1.5) = 3.0, total= 4.0
Both halves equal 4.0, confirming the line at y = 1.5 splits the combined area perfectly.
Key takeaway: Notice how the overlapping region (1 ≤ y ≤ 2) was counted twice—once for Square A and once for Square B—exactly as the problem requires. This happens naturally because each square's contribution is added independently in the check function, without ever subtracting shared regions.
Solution Implementation
1from typing import List
2
3
4class Solution:
5 def separateSquares(self, squares: List[List[int]]) -> float:
6 # Compute the area of all squares that lies below a given horizontal line y = line_y.
7 # If this area is at least half of the total area, the line is high enough.
8 def is_line_high_enough(line_y: float) -> bool:
9 area_below = 0.0
10 for _, bottom_y, side in squares:
11 # Only consider squares whose bottom edge is below the line.
12 if bottom_y < line_y:
13 # The covered height is capped by the square's side length.
14 covered_height = min(line_y - bottom_y, side)
15 area_below += side * covered_height
16 return area_below >= total_area / 2
17
18 # Total area of all squares (side * side for each).
19 total_area = sum(side * side for *_, side in squares)
20
21 # Binary search bounds: from the lowest possible y to the highest top edge.
22 low, high = 0.0, max(bottom_y + side for _, bottom_y, side in squares)
23 epsilon = 1e-5
24
25 # Binary search for the smallest y such that the area below is at least half.
26 while high - low > epsilon:
27 mid = (low + high) / 2
28 if is_line_high_enough(mid):
29 high = mid
30 else:
31 low = mid
32
33 return high
341class Solution {
2 // The list of squares, where each square is represented as [x, y, sideLength].
3 private int[][] squares;
4 // The total area of all squares combined.
5 private double totalArea;
6
7 /**
8 * Checks whether a horizontal line at height {@code lineY} splits the squares
9 * such that the area below the line is at least half of the total area.
10 *
11 * @param lineY the candidate y-coordinate of the horizontal line
12 * @return true if the area below {@code lineY} is at least half the total area
13 */
14 private boolean check(double lineY) {
15 double areaBelow = 0.0;
16 for (int[] square : squares) {
17 int bottomY = square[1];
18 int sideLength = square[2];
19 // Only squares whose bottom edge is below the line contribute area.
20 if (bottomY < lineY) {
21 // The covered height is capped by the square's side length.
22 areaBelow += (double) sideLength * Math.min(lineY - bottomY, sideLength);
23 }
24 }
25 return areaBelow >= totalArea / 2.0;
26 }
27
28 /**
29 * Finds the minimum y-coordinate of a horizontal line that divides the
30 * total area of all squares into two equal halves.
31 *
32 * @param squares the array of squares, each as [x, y, sideLength]
33 * @return the y-coordinate of the dividing horizontal line
34 */
35 public double separateSquares(int[][] squares) {
36 this.squares = squares;
37 totalArea = 0.0;
38
39 // Binary search bounds on the y-coordinate of the dividing line.
40 double low = 0.0;
41 double high = 0.0;
42 for (int[] square : squares) {
43 // Accumulate the area of each square (side length squared).
44 totalArea += (double) square[2] * square[2];
45 // The upper bound is the highest top edge among all squares.
46 high = Math.max(high, square[1] + square[2]);
47 }
48
49 // Binary search for the smallest line height satisfying the area condition.
50 double eps = 1e-5;
51 while (high - low > eps) {
52 double mid = (low + high) / 2.0;
53 if (check(mid)) {
54 high = mid;
55 } else {
56 low = mid;
57 }
58 }
59 return high;
60 }
61}
621class Solution {
2public:
3 // Pointer to the input squares for use inside the helper function
4 vector<vector<int>>* squares;
5 // Total area of all squares combined
6 double totalArea;
7
8 // Checks whether the horizontal line y = lineY splits the squares such that
9 // the area below the line is at least half of the total area
10 bool check(double lineY) {
11 double areaBelow = 0.0;
12 for (const auto& square : *squares) {
13 int bottomY = square[1]; // y-coordinate of the square's bottom edge
14 int sideLength = square[2]; // side length of the square
15
16 // Only squares whose bottom edge lies below the line contribute area
17 if (bottomY < lineY) {
18 // The vertical overlap is capped by the square's side length,
19 // multiplied by the width (also the side length) to get the area
20 areaBelow += (double) sideLength * min(lineY - bottomY, (double) sideLength);
21 }
22 }
23 // True if the area below the line reaches at least half the total area
24 return areaBelow >= totalArea / 2.0;
25 }
26
27 double separateSquares(vector<vector<int>>& squares) {
28 this->squares = &squares;
29 totalArea = 0.0;
30
31 double low = 0.0; // Lower bound of the search interval for the dividing line
32 double high = 0.0; // Upper bound of the search interval for the dividing line
33
34 // Compute the total area and find the highest top edge to bound the search
35 for (const auto& square : squares) {
36 totalArea += (double) square[2] * square[2]; // accumulate area (side^2)
37 high = max(high, (double) square[1] + square[2]); // bottom + side = top edge
38 }
39
40 const double eps = 1e-5; // Precision threshold for binary search
41
42 // Binary search for the smallest line position that satisfies the half-area condition
43 while (high - low > eps) {
44 double mid = (low + high) / 2.0;
45 if (check(mid)) {
46 // Line is high enough (or too high); try lowering it
47 high = mid;
48 } else {
49 // Not enough area below; raise the line
50 low = mid;
51 }
52 }
53
54 // Either bound is acceptable within the epsilon tolerance
55 return high;
56 }
57};
581/**
2 * Finds a horizontal line y = result such that the total area of squares
3 * below the line equals the total area of squares above the line.
4 *
5 * @param squares - Each square is [x, y, sideLength], where (x, y) is the
6 * bottom-left corner and sideLength is the side length.
7 * @returns The y-coordinate of the dividing horizontal line.
8 */
9function separateSquares(squares: number[][]): number {
10 /**
11 * Checks whether the area of all squares lying below the line y = lineY
12 * is at least half of the total area.
13 *
14 * @param lineY - The candidate y-coordinate of the horizontal line.
15 * @returns True if the area below the line is >= half of the total area.
16 */
17 const check = (lineY: number): boolean => {
18 let areaBelow = 0;
19 for (const [, bottomY, sideLength] of squares) {
20 // Only squares whose bottom edge is below the line contribute.
21 if (bottomY < lineY) {
22 // The covered vertical height is capped by the square's side length.
23 const coveredHeight = Math.min(lineY - bottomY, sideLength);
24 areaBelow += sideLength * coveredHeight;
25 }
26 }
27 return areaBelow >= totalArea / 2;
28 };
29
30 // Total area of all squares and the binary search boundaries.
31 let totalArea = 0;
32 let low = 0;
33 let high = 0;
34 for (const [, bottomY, sideLength] of squares) {
35 totalArea += sideLength * sideLength;
36 // The highest possible line is the top edge of the topmost square.
37 high = Math.max(high, bottomY + sideLength);
38 }
39
40 // Binary search on the answer with a precision tolerance.
41 const eps = 1e-5;
42 while (high - low > eps) {
43 const mid = (low + high) / 2;
44 if (check(mid)) {
45 // Area below is already >= half, so move the line downward.
46 high = mid;
47 } else {
48 // Area below is too small, so move the line upward.
49 low = mid;
50 }
51 }
52
53 return high;
54}
55Time and Space Complexity
Time Complexity: O(n log(MU))
Let's break down the analysis step by step:
-
Initial setup:
- Computing
s = sum(a[2] * a[2] for a in squares)iterates over all squares once, takingO(n)time. - Computing
r = max(a[1] + a[2] for a in squares)also iterates over all squares once, takingO(n)time.
- Computing
-
Binary search on the answer:
- The binary search runs over the continuous range
[0, r], wherer = max(a[1] + a[2]). Since the coordinate values are bounded byM = 10^5and the maximum upper bound isU = max(y_i + l_i), the search range is on the order ofMU. - The loop continues while
r - l > eps, whereeps = 1e-5. Each iteration halves the search interval, so the number of iterations isO(log((MU) / eps)), which simplifies toO(log(MU))(the constant factor fromepsis absorbed).
- The binary search runs over the continuous range
-
The
checkfunction:- Each call to
check(y1)iterates over allnsquares to accumulate the area below the liney1, takingO(n)time per call.
- Each call to
-
Combining:
- The binary search performs
O(log(MU))iterations, and each iteration callscheck, which costsO(n). - Therefore, the total time complexity is
O(n log(MU)), which dominates theO(n)initial setup.
- The binary search performs
Space Complexity: O(1)
- The algorithm only uses a constant number of extra variables (
s,l,r,mid,eps,t), and thecheckfunction uses constant auxiliary space. - No additional data structures that scale with the input size are allocated, so the space complexity is
O(1).
Pattern Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall 1: Using too loose an epsilon for the binary search precision
A frequent mistake is setting epsilon to a value like 1e-5 and assuming this directly maps to the 10^-5 accuracy required by the problem. The catch is that the binary search converges on the y-coordinate, but the accepted tolerance is on the final answer's value. When the side lengths are large (e.g., up to 10^9), a small error in y can be acceptable, but the loop termination condition high - low > epsilon may stop too early or accumulate floating-point error, causing the returned y to drift outside the tolerance.
Why it happens:
- The area function
area_belowscales withside * side. For large coordinates, the values involved are huge, so floating-point subtractionline_y - bottom_yloses precision. - Relying on
high - low > epsilonties the iteration count to the magnitude ofhigh. Ifhighis2 * 10^9, reaching1e-5precision requireslog2(2e9 / 1e-5) ≈ 48iterations — manageable, but the precision near the answer can still be marginal.
Solution: Use a fixed number of iterations instead of an absolute-difference condition. Running a fixed count (e.g., 100 iterations) guarantees the interval shrinks by a factor of 2^100, far exceeding any precision need, and avoids subtle termination issues:
from typing import List
class Solution:
def separateSquares(self, squares: List[List[int]]) -> float:
def is_line_high_enough(line_y: float) -> bool:
area_below = 0.0
for _, bottom_y, side in squares:
if bottom_y < line_y:
covered_height = min(line_y - bottom_y, side)
area_below += side * covered_height
return area_below >= total_area / 2
total_area = sum(side * side for *_, side in squares)
low, high = 0.0, max(bottom_y + side for _, bottom_y, side in squares)
# Fixed iteration count: 100 halvings shrink any range below any practical eps.
for _ in range(100):
mid = (low + high) / 2
if is_line_high_enough(mid):
high = mid
else:
low = mid
return high
Pitfall 2: Misjudging the strict vs. non-strict comparison bottom_y < line_y
It may seem harmless to write bottom_y <= line_y instead of bottom_y < line_y. While in this specific code the result is the same (when bottom_y == line_y, the covered height min(line_y - bottom_y, side) is 0, contributing nothing), developers sometimes flip the logic and accidentally compute the area above the line, or forget the boundary entirely and process all squares unconditionally — leading to negative covered heights when line_y < bottom_y.
Why it happens:
- Without the
if bottom_y < line_yguard,line_y - bottom_ycan be negative, andmin(negative, side)yields a negative value, incorrectly subtracting area.
Solution: Always guard the contribution, or equivalently clamp the height to a non-negative range:
covered_height = max(0.0, min(line_y - bottom_y, side))
area_below += side * covered_height
This max(0.0, ...) clamp makes the function robust even if the guard condition is removed or refactored.
Pitfall 3: Forgetting that overlaps must be double-counted
The problem explicitly states overlapping regions are counted once per square. A common instinct is to merge overlapping geometry (as in classic "rectangle area" or sweep-line union problems) to avoid double counting. Applying that here would compute the wrong total area and break the equal-split condition.
Why it happens:
- Many similar LeetCode problems (e.g., "Rectangle Area II") require computing the union area, conditioning developers to deduplicate overlaps reflexively.
Solution: Treat each square fully independently — sum side * side for the total and add each square's below-line contribution separately, exactly as the code does. No union or interval-merging logic should be introduced. The independence of each square's contribution is precisely what makes the simple O(n) check function correct.
Ready to land your dream job?
Unlock your dream job with a 5-minute quiz for a personalized study roadmap!
Get My RoadmapWhich of the following uses divide and conquer strategy?
Recommended Readings
https assets algo monster cover_photos Binary_Search svg Binary Search Intuition Binary search is an efficient array search algorithm It works by narrowing down the search range by half each time If you have looked up a word in a physical dictionary you've already used binary search in real life Let's
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion If you prefer videos here's a video that explains recursion in a fun and easy way Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask him
Want a Structured Path to Master System Design Too? Don’t Miss This!