3111. Minimum Rectangles to Cover Points
Problem Description
You are given a set of 2D points represented as points[i] = [xi, yi]
and a width constraint w
. Your task is to cover all these points using the minimum number of rectangles.
Each rectangle must follow these rules:
- The bottom edge starts at some point
(x1, 0)
on the x-axis - The top edge ends at some point
(x2, y2)
wherey2 >= 0
- The width of each rectangle is limited:
x2 - x1 <= w
- The rectangle extends infinitely in the vertical direction (from y = 0 upwards)
A point is considered covered if it lies inside or on the boundary of at least one rectangle. Points can be covered by multiple rectangles.
The key insight is that since rectangles extend infinitely in the vertical direction, we only need to worry about covering points based on their x-coordinates. The solution sorts points by x-coordinate and greedily places rectangles. Starting from the leftmost point, each rectangle covers as many consecutive points as possible within the width constraint w
. When a point's x-coordinate exceeds the current rectangle's right boundary, a new rectangle is needed.
For example, if we have points at x-coordinates [1, 3, 5, 8] with w = 3
:
- First rectangle covers x from 1 to 4 (covers points at x = 1 and x = 3)
- Second rectangle covers x from 5 to 8 (covers points at x = 5 and x = 8)
- Total rectangles needed: 2
Intuition
The first observation is that the y-coordinates of the points don't matter for determining the minimum number of rectangles. Since each rectangle extends infinitely upward from y = 0
and has no height restriction, any rectangle that covers a point's x-coordinate will automatically cover it regardless of its y-coordinate.
This transforms the 2D problem into a 1D problem: we only need to ensure all x-coordinates are covered by rectangles of width at most w
.
Next, we ask: what's the optimal way to place rectangles to minimize their count? The greedy approach emerges naturally - we should make each rectangle cover as many points as possible.
To implement this greedy strategy:
- Sort points by x-coordinate so we can process them from left to right
- Place the first rectangle starting at the leftmost point
- Extend this rectangle as far right as possible (up to width
w
) - When we encounter a point that can't be covered by the current rectangle, we must start a new rectangle
Why does this greedy approach work? Consider any optimal solution. If we have a rectangle that doesn't extend as far right as possible, we can always shift it right without losing coverage of any points, potentially covering more points to the right. This means the greedy choice of maximizing each rectangle's coverage is always optimal.
The algorithm tracks x1
as the rightmost boundary of the current rectangle. When we encounter a point at position x
where x > x1
, we know we need a new rectangle. We place it starting at x
and extending to x + w
, updating x1 = x + w
for future comparisons.
Solution Approach
The implementation follows a greedy algorithm with sorting:
Step 1: Sort the points
points.sort()
We sort all points by their x-coordinates (Python's default sorting for lists of lists sorts by the first element). This allows us to process points from left to right, ensuring we don't miss any points and can make optimal greedy decisions.
Step 2: Initialize tracking variables
ans, x1 = 0, -1
ans
: Counts the number of rectangles usedx1
: Tracks the rightmost x-coordinate that the current rectangle can cover. Initialized to -1 so the first point will always trigger a new rectangle
Step 3: Iterate through sorted points
for x, _ in points: if x > x1: ans += 1 x1 = x + w
For each point (x, y)
:
- We ignore the y-coordinate (using
_
) since height doesn't matter - Check if the current point's x-coordinate exceeds
x1
(the right boundary of the current rectangle) - If
x > x1
, the point cannot be covered by the current rectangle:- Increment
ans
by 1 (we need a new rectangle) - Update
x1 = x + w
(the new rectangle starts atx
and extends tox + w
)
- Increment
- If
x <= x1
, the point is already covered by the current rectangle, so we continue
Step 4: Return the result
return ans
Time Complexity: O(n log n)
where n is the number of points, dominated by the sorting step
Space Complexity: O(1)
if we don't count the space used for sorting (typically O(log n)
for the sorting algorithm's recursion stack)
The algorithm ensures minimal rectangles by maximizing the coverage of each rectangle before placing a new one.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the algorithm with a concrete example:
Input: points = [[2, 1], [1, 0], [1, 4], [1, 8], [3, 5], [4, 6]]
, w = 1
Step 1: Sort the points by x-coordinate
After sorting: [[1, 0], [1, 4], [1, 8], [2, 1], [3, 5], [4, 6]]
Step 2: Initialize variables
ans = 0
(rectangle count)x1 = -1
(rightmost boundary of current rectangle)
Step 3: Process each point
Point [1, 0]:
- x = 1, current x1 = -1
- Since 1 > -1, we need a new rectangle
ans = 1
,x1 = 1 + 1 = 2
- Rectangle 1 covers x โ [1, 2]
Point [1, 4]:
- x = 1, current x1 = 2
- Since 1 โค 2, this point is covered by Rectangle 1
- No change needed
Point [1, 8]:
- x = 1, current x1 = 2
- Since 1 โค 2, this point is covered by Rectangle 1
- No change needed
Point [2, 1]:
- x = 2, current x1 = 2
- Since 2 โค 2, this point is covered by Rectangle 1
- No change needed
Point [3, 5]:
- x = 3, current x1 = 2
- Since 3 > 2, we need a new rectangle
ans = 2
,x1 = 3 + 1 = 4
- Rectangle 2 covers x โ [3, 4]
Point [4, 6]:
- x = 4, current x1 = 4
- Since 4 โค 4, this point is covered by Rectangle 2
- No change needed
Step 4: Return result
ans = 2
Visual representation:
Rectangle 1: x โ [1, 2] covers points at x = 1 and x = 2 Rectangle 2: x โ [3, 4] covers points at x = 3 and x = 4
The algorithm successfully covers all 6 points using just 2 rectangles, each with width โค 1.
Solution Implementation
1class Solution:
2 def minRectanglesToCoverPoints(self, points: List[List[int]], w: int) -> int:
3 # Sort points by x-coordinate to process them from left to right
4 points.sort()
5
6 # Initialize rectangle count and rightmost boundary of current rectangle
7 rectangle_count = 0
8 current_right_boundary = -1
9
10 # Iterate through all points (only x-coordinate matters for vertical rectangles)
11 for x_coord, _ in points:
12 # Check if current point is outside the current rectangle's coverage
13 if x_coord > current_right_boundary:
14 # Need a new rectangle to cover this point
15 rectangle_count += 1
16 # Set the new rectangle's right boundary (extends w units from current x)
17 current_right_boundary = x_coord + w
18
19 return rectangle_count
20
1class Solution {
2 public int minRectanglesToCoverPoints(int[][] points, int w) {
3 // Sort points by x-coordinate in ascending order
4 Arrays.sort(points, (a, b) -> a[0] - b[0]);
5
6 // Initialize rectangle count and right boundary of current rectangle
7 int rectangleCount = 0;
8 int currentRectangleRightBoundary = -1;
9
10 // Iterate through all points in sorted order
11 for (int[] point : points) {
12 int currentX = point[0];
13
14 // If current point is beyond the right boundary of the current rectangle,
15 // we need a new rectangle
16 if (currentX > currentRectangleRightBoundary) {
17 rectangleCount++;
18 // Set the right boundary of the new rectangle
19 // Rectangle spans from currentX to currentX + w
20 currentRectangleRightBoundary = currentX + w;
21 }
22 }
23
24 return rectangleCount;
25 }
26}
27
1class Solution {
2public:
3 int minRectanglesToCoverPoints(vector<vector<int>>& points, int w) {
4 // Sort points by x-coordinate in ascending order
5 sort(points.begin(), points.end());
6
7 // Initialize the count of rectangles needed
8 int rectangleCount = 0;
9
10 // Track the rightmost x-coordinate covered by the current rectangle
11 int currentRightBoundary = -1;
12
13 // Iterate through all points in sorted order
14 for (const auto& point : points) {
15 int xCoordinate = point[0];
16
17 // If current point is not covered by the existing rectangle
18 if (xCoordinate > currentRightBoundary) {
19 // We need a new rectangle
20 rectangleCount++;
21
22 // Update the right boundary of the new rectangle
23 // The rectangle extends from xCoordinate to xCoordinate + w
24 currentRightBoundary = xCoordinate + w;
25 }
26 }
27
28 return rectangleCount;
29 }
30};
31
1/**
2 * Calculates the minimum number of rectangles needed to cover all points
3 * @param points - Array of 2D points where each point is [x, y]
4 * @param w - Width of each rectangle
5 * @returns The minimum number of rectangles needed
6 */
7function minRectanglesToCoverPoints(points: number[][], w: number): number {
8 // Sort points by x-coordinate in ascending order
9 points.sort((pointA: number[], pointB: number[]) => pointA[0] - pointB[0]);
10
11 // Initialize rectangle count and rightmost boundary of current rectangle
12 let rectangleCount: number = 0;
13 let currentRectangleRightBoundary: number = -1;
14
15 // Iterate through each point in sorted order
16 for (const point of points) {
17 const xCoordinate: number = point[0];
18
19 // If current point is beyond the right boundary of current rectangle
20 if (xCoordinate > currentRectangleRightBoundary) {
21 // Place a new rectangle starting at this point
22 rectangleCount++;
23 // Update the right boundary to be x + width
24 currentRectangleRightBoundary = xCoordinate + w;
25 }
26 }
27
28 return rectangleCount;
29}
30
Time and Space Complexity
The time complexity of this algorithm is O(n ร log n)
, where n
is the number of points. This is dominated by the sorting operation points.sort()
, which uses a comparison-based sorting algorithm (typically Timsort in Python) that has O(n ร log n)
time complexity. The subsequent iteration through the sorted points takes O(n)
time, but this is absorbed by the larger sorting complexity.
The space complexity is O(log n)
. While the iteration through points uses only O(1)
extra space for variables like ans
and x1
, the sorting operation requires O(log n)
space for the recursion stack in the sorting algorithm. Python's Timsort uses a hybrid approach with merge sort that requires O(log n)
space for maintaining the recursion stack during the divide-and-conquer process.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Misunderstanding Rectangle Placement Strategy
Pitfall: A common mistake is placing rectangles starting from the leftmost possible position that covers the first uncovered point, rather than starting the rectangle exactly at the point's x-coordinate.
Incorrect approach:
# Wrong: Starting rectangle at leftmost possible position for x_coord, _ in points: if x_coord > current_right_boundary: rectangle_count += 1 # This tries to maximize coverage by starting as far left as possible current_right_boundary = current_right_boundary + w # WRONG!
Why it fails: Consider points at x = [1, 5, 10] with w = 3:
- First rectangle covers x = 1, right boundary = 4
- Point at x = 5 is not covered (5 > 4)
- If we place the next rectangle starting at x = 4, it covers up to x = 7
- Point at x = 10 still needs another rectangle
- Total: 3 rectangles
Correct approach:
# Correct: Start rectangle at the current point's position for x_coord, _ in points: if x_coord > current_right_boundary: rectangle_count += 1 current_right_boundary = x_coord + w # Start at current point
With the correct approach for the same example:
- First rectangle starts at x = 1, covers up to x = 4
- Second rectangle starts at x = 5, covers up to x = 8
- Third rectangle starts at x = 10, covers up to x = 13
- Total: 3 rectangles (same result in this case, but the strategy is more reliable)
2. Handling Duplicate X-Coordinates
Pitfall: Not considering that multiple points can have the same x-coordinate.
Issue: While the current solution handles this correctly (sorting preserves all points and processes them sequentially), developers might incorrectly try to optimize by removing duplicates:
# Wrong: Removing duplicate x-coordinates
unique_x = list(set([x for x, _ in points]))
unique_x.sort()
Why it matters: Even though points with the same x-coordinate will be covered by the same rectangle, removing duplicates can lead to incorrect logic if you later need to track which specific points are covered or if the problem requirements change.
Best practice: Keep all points and let the algorithm naturally handle duplicates:
# Correct: Process all points, duplicates are naturally handled points.sort() for x_coord, _ in points: # Duplicates with same x will simply not trigger new rectangles if x_coord > current_right_boundary: rectangle_count += 1 current_right_boundary = x_coord + w
3. Integer Overflow or Precision Issues
Pitfall: Not considering the range of coordinates or using incorrect data types.
Issue: If coordinates can be very large or include floating-point values, simple addition might cause issues:
# Potential issue with large numbers or floats current_right_boundary = x_coord + w
Solution: Be aware of the problem constraints and handle edge cases:
def minRectanglesToCoverPoints(self, points: List[List[int]], w: int) -> int:
# Check for edge cases
if not points:
return 0
if w < 0:
raise ValueError("Width must be non-negative")
points.sort()
rectangle_count = 0
current_right_boundary = float('-inf') # Use float for safety
for x_coord, _ in points:
if x_coord > current_right_boundary:
rectangle_count += 1
# For very large numbers, ensure no overflow
current_right_boundary = min(x_coord + w, float('inf'))
return rectangle_count
What data structure does Breadth-first search typically uses to store intermediate states?
Recommended Readings
Greedy Introduction div class responsive iframe iframe src https www youtube com embed WTslqPbj7I title YouTube video player frameborder 0 allow accelerometer autoplay clipboard write encrypted media gyroscope picture in picture web share allowfullscreen iframe div When do we use greedy Greedy algorithms tend to solve optimization problems Typically they will ask you to calculate the max min of some value Commonly you may see this phrased in the problem as max min longest shortest largest smallest etc These keywords can be identified by just scanning
Sorting Summary Comparisons We presented quite a few sorting algorithms and it is essential to know the advantages and disadvantages of each one The basic algorithms are easy to visualize and easy to learn for beginner programmers because of their simplicity As such they will suffice if you don't know any advanced
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
Want a Structured Path to Master System Design Too? Donโt Miss This!