Topics
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 parallel to the x-axis.
Find the minimum y-coordinate value of a horizontal line such that the total area of the squares above the line equals the total area of the squares below the line. Answers within of the actual answer will be accepted.
Note: Squares may overlap. Overlapping areas should be counted multiple times.
Idea
We can use binary search on the y-coordinate. The core idea is to define a check(y)
function that determines if, for a given y-coordinate, the area below the line is greater than or equal to the area above the line. This leverages the binary search boolean array framework. Since we are dealing with floating point numbers, we use binary search on real domain.
Why Binary Search Works:
There’s a monotonic relationship between the line’s y-coordinate and the areas above/below it. As y
increases, the area below increases, and the area above decreases (and vice-versa). This allows us to use binary search. The check(y)
function effectively tests the boolean condition below >= above
. We use >=
instead of ==
because, due to the discrete nature of squares, there might not be a single y
where areas are exactly equal. We seek the minimum y
where below >= above
, the “tipping point”.
Approach 1 (Direct Binary Search)
This approach directly calculates the areas above and below the line for each candidate y
in the binary search.
- Iterate through each square:
- Three Cases:
- Square completely below:
y_hi <= y
. Add the entire square’s area tobelow
- Square completely above:
y_lo >= y
. Add the entire square’s area toabove
- Square intersects the line: Calculate the area below (
(y - y_lo) * side
) and the area above ((y_hi - y) * side
) and add them to the respective totals
- Square completely below:
- Return
below >= above
: This indicates whether the currenty
satisfies the condition
Time Complexity: , where n is the number of squares.
Space Complexity:
Approach 2 (Prefix Sums + Binary Search)
This approach optimizes the area calculation using prefix sums to avoid looping all the squares within the check
function.
Preprocessing:
- Sort by Bottom Y: Sort
squares
by their bottom y-coordinate (squares[i][1]
) bottomYs
: Store the sorted bottom y-coordinates- Sort by Top Y: Create
topPairs
(pairs of {top y, side}) and sort it. ExtracttopYs
from the sortedtopPairs
- Prefix Sums (Bottom Y Sorted):
prefixArea
: Cumulative sum of areas (side * side
)prefixWeightedTop
: Cumulative sum oftop * side
. This helps calculate the area above the line for intersecting squaresprefixSide
: Cumulative sum of side lengths
- Prefix Sums (Top Y Sorted):
prefixWeightedTopV2
: Cumulative sum oftop * side
(on top y sorted squares)prefixSideTop
: Cumulative sum of side lengths (on top y sorted squares)
The check
function:
- Binary Search on
bottomYs
: Find the indexidx
of the first square whose bottom y is greater than or equal to the currenty
. - Area Below:
prefixArea[idx]
gives the total area of squares that “start” belowy
- Area Above:
prefixArea.back() - prefixArea[idx]
gives the total area of squares completely abovey
- Intersecting Squares (Above):
partialAbove = prefixWeightedTop[idx] - y * prefixSide[idx]
. This efficiently calculates the total area abovey
for squares that intersect the line. But this incorrectly, also takes into account the squares that are entirely below the line. Hence, we need to subtract/adjust this “extra”
- Adjustment for Squares Completely Below (Top Y):
- Binary search on
topYs
to findidxTop
(last square that lies completely below line) partialAboveAdjustment = prefixWeightedTopV2[idxTop] - y * prefixSideTop[idxTop]
. Subtracts the contribution of squares that are entirely below the line (based on top Y)
- Binary search on
- Cut Adjustment:
cutAdjustment = partialAbove - partialAboveAdjustment
- Final Correct Areas:
areaBelow -= cutAdjustment
,areaAbove += cutAdjustment
- Return
below >= above
Time Complexity: . The comes from sorting, and the binary searches within check
contribute the second term.
Space Complexity: due to the prefix sum arrays.
Code
# Approach 1 (Binary Search)
bool check(double y, vector<vi> &squares) {
double below = 0, above = 0;
for (vi &square : squares) {
int y_lo = square[1];
int y_hi = square[1] + square[2];
if (y_hi <= y) {
// below line
below += 1.0 * square[2] * square[2];
} else if (y_lo >= y) {
// above line
above += 1.0 * square[2] * square[2];
} else {
// line cuts through
below += (y - y_lo) * square[2];
above += (y_hi - y) * square[2];
}
}
return below >= above;
}
double solve(vector<vi> &squares) {
int max_iters = 50;
double lo = 0;
double hi = 1e9+5;
double ans = hi;
double eps = 1e-5;
for (int i = 0; i < max_iters; ++i) {
double mid = lo + (hi - lo) / 2.0;
if (check(mid, squares)) {
ans = mid;
hi = mid - eps;
} else {
lo = mid + eps;
}
}
return ans;
}
# Approach 2 (prefix sums and range queries)
bool check(long double y, const vector<long double> &bottomYs, const vll &prefixArea,
const vll &prefixWeightedTop, const vll &prefixSide,
const vector<long double> &topYs, const vll &prefixWeightedTopV2,
const vll &prefixSideTop) {
// Find the first square (in squares sorted by bottom y) whose bottom is >= y.
int idx = lower_bound(all(bottomYs), y) - bottomYs.begin();
// Squares with bottom < y contribute their full area to "below" the line.
long double areaBelow = prefixArea[idx];
long double areaAbove = prefixArea.back() - prefixArea[idx];
// For squares that start below y, compute the "extra" area from the portion
// of the square that lies above y. Our prefixWeightedTop stores (bottom +
// side)*side for each square. The contribution of a square with bottom b, top t and
// side l that is cut by y is:
// (t - y) * l = (b + l) * l - y * l.
long double partialAbove = prefixWeightedTop[idx] - y * prefixSide[idx];
// However, for squares that are completely below y (when considering their
// top), we must subtract their contributions since they were incorrectly accounted for in the previous calculation. We do a binary search on the sorted topYs array to find squares that are completely below y
int idxTop = lower_bound(all(topYs), y) - topYs.begin();
long double partialAboveAdjustment = prefixWeightedTopV2[idxTop] - y * prefixSideTop[idxTop];
// The net extra area from squares that are cut by the line.
long double cutAdjustment = partialAbove - partialAboveAdjustment;
// Remove the "cut" area from below and add it to above.
areaBelow -= cutAdjustment;
areaAbove += cutAdjustment;
return areaBelow >= areaAbove;
}
double solve(vector<vi> &squares) {
int n = squares.size();
// Sort squares by their bottom y-coordinate.
sort(squares.begin(), squares.end(),
[](const vi &a, const vi &b) { return a[1] < b[1]; });
// Precompute the bottom y-coordinates for binary search.
vector<long double> bottomYs(n);
for (int i = 0; i < n; ++i) {
bottomYs[i] = squares[i][1];
}
// Prepare an array for squares sorted by top y-coordinate (top = y + side).
// We store pairs: {top, side}
vector<pair<long double, int>> topPairs(n);
for (int i = 0; i < n; ++i) {
int y = squares[i][1], side = squares[i][2];
topPairs[i] = {y + side, side};
}
sort(topPairs.begin(), topPairs.end());
// Also extract the sorted top values.
vector<long double> topYs(n);
for (int i = 0; i < n; ++i) {
topYs[i] = topPairs[i].first;
}
// Precompute prefix sums for squares sorted by bottom y:
// 1. prefixArea: cumulative sum of square areas (side^2)
// 2. prefixWeightedTop: cumulative sum of top*side [top = bottom + side]
// 3. prefixSide: cumulative sum of side lengths.
vll prefixArea(n + 1, 0), prefixWeightedTop(n + 1, 0),
prefixSide(n + 1, 0);
for (int i = 0; i < n; ++i) {
ll side = squares[i][2];
prefixArea[i + 1] = prefixArea[i] + side * side;
prefixWeightedTop[i + 1] =
prefixWeightedTop[i] + (squares[i][1] + side) * side;
prefixSide[i + 1] = prefixSide[i] + side;
}
// Precompute prefix sums for squares sorted by top y:
// 1. prefixWeightedTopV2: cumulative sum of (top) * side
// 2. prefixSideTop: cumulative sum of side lengths.
vll prefixWeightedTopV2(n + 1, 0), prefixSideTop(n + 1, 0);
for (int i = 0; i < n; ++i) {
ll side = topPairs[i].second;
prefixWeightedTopV2[i + 1] = prefixWeightedTopV2[i] + topPairs[i].first * side;
prefixSideTop[i + 1] = prefixSideTop[i] + side;
}
// Binary search for the minimum y that balances the area.
// The search range is from 0 to the maximum top among the squares.
double lo = 0.0, hi = topYs.back(); // hi can be set to 1e9+5 as well
double ans = hi;
double eps = 1e-5;
// We run a fixed number of iterations (60) to ensure the answer converges
// within the acceptable error of 1e-5.
for (int iter = 0; iter < 50; ++iter) {
long double mid = lo + (hi - lo) / 2.0;
if (check(mid, bottomYs, prefixArea, prefixWeightedTop, prefixSide,
topYs, prefixWeightedTopV2, prefixSideTop)) {
ans = mid;
hi = mid - eps;
} else {
lo = mid + eps;
}
}
return ans;
}