Facebook Pixel

3453. Separate Squares I

MediumArrayBinary Search
LeetCode ↗

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 than y is considered "above" the line, and everything with a y-coordinate less than y is 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^-5 of 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

How We Pick the Algorithm

Why Binary Search?

This problem maps to Binary Search through a short path in the full flowchart.

Sortedinput ormonotonicyesDynamicrangequeries?noBinary Search

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 Flowchart

Intuition

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 edge y is below y1, then part (or all) of it lies under the line.
  • The height of the portion below the line is min(y1 - y, l). When y1 - y exceeds l, the entire square is below the line, so the height is capped at l.
  • Multiplying this height by the side length l gives that square's contribution l * min(y1 - y, l), which we accumulate into t.

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) is True, the area below is already at least half, so the answer is at this height or lower. We move the right boundary down with r = 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 length 2, so it spans y from 0 to 2.
  • Square B: bottom-left at (1, 1) with side length 2, so it spans y from 1 to 3. (Notice it overlaps with Square A in the region 1 ≤ 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, contributes 2 * 1.5 = 3.0
  • Square B: min(1.5 - 1, 2) = 0.5, contributes 2 * 0.5 = 1.0
  • Area below = 3.0 + 1.0 = 4.0, which is ≥ 4True
  • 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, contributes 2 * 0.75 = 1.5
  • Square B: bottom y = 1 is not below 0.75, so it contributes 0
  • Area below = 1.5, which is < 4False
  • 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, contributes 2 * 1.125 = 2.25
  • Square B: min(1.125 - 1, 2) = 0.125, contributes 2 * 0.125 = 0.25
  • Area below = 2.25 + 0.25 = 2.5, which is < 4False
  • 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, contributes 2.625
  • Square B: min(0.3125, 2) = 0.3125, contributes 0.625
  • Area below = 3.25, which is < 4False
  • 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 contributes 2 * (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
34
1class 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}
62
1class 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};
58
1/**
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}
55

Time and Space Complexity

Time Complexity: O(n log(MU))

Let's break down the analysis step by step:

  1. Initial setup:

    • Computing s = sum(a[2] * a[2] for a in squares) iterates over all squares once, taking O(n) time.
    • Computing r = max(a[1] + a[2] for a in squares) also iterates over all squares once, taking O(n) time.
  2. Binary search on the answer:

    • The binary search runs over the continuous range [0, r], where r = max(a[1] + a[2]). Since the coordinate values are bounded by M = 10^5 and the maximum upper bound is U = max(y_i + l_i), the search range is on the order of MU.
    • The loop continues while r - l > eps, where eps = 1e-5. Each iteration halves the search interval, so the number of iterations is O(log((MU) / eps)), which simplifies to O(log(MU)) (the constant factor from eps is absorbed).
  3. The check function:

    • Each call to check(y1) iterates over all n squares to accumulate the area below the line y1, taking O(n) time per call.
  4. Combining:

    • The binary search performs O(log(MU)) iterations, and each iteration calls check, which costs O(n).
    • Therefore, the total time complexity is O(n log(MU)), which dominates the O(n) initial setup.

Space Complexity: O(1)

  • The algorithm only uses a constant number of extra variables (s, l, r, mid, eps, t), and the check function 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_below scales with side * side. For large coordinates, the values involved are huge, so floating-point subtraction line_y - bottom_y loses precision.
  • Relying on high - low > epsilon ties the iteration count to the magnitude of high. If high is 2 * 10^9, reaching 1e-5 precision requires log2(2e9 / 1e-5) ≈ 48 iterations — 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_y guard, line_y - bottom_y can be negative, and min(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 Roadmap
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Get a Personalized Study Roadmap:

Which of the following uses divide and conquer strategy?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More