Facebook Pixel

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) where y2 >= 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
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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:

  1. Sort points by x-coordinate so we can process them from left to right
  2. Place the first rectangle starting at the leftmost point
  3. Extend this rectangle as far right as possible (up to width w)
  4. 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.

Learn more about Greedy and Sorting patterns.

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 used
  • x1: 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 at x and extends to x + w)
  • 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 Evaluator

Example 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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What data structure does Breadth-first search typically uses to store intermediate states?


Recommended Readings

Want a Structured Path to Master System Design Too? Donโ€™t Miss This!

Load More