Facebook Pixel

497. Random Point in Non-overlapping Rectangles

MediumReservoir SamplingArrayMathBinary SearchOrdered SetPrefix SumRandomized
Leetcode Link

Problem Description

You are given an array of non-overlapping axis-aligned rectangles where each rectangle is defined by four coordinates: [a_i, b_i, x_i, y_i]. The point (a_i, b_i) represents the bottom-left corner and (x_i, y_i) represents the top-right corner of the i-th rectangle.

Your task is to design a data structure that can randomly pick an integer point from the space covered by all the rectangles. The key requirements are:

  1. Integer Points Only: You must return points with integer coordinates (e.g., [3, 5] not [3.5, 5.2])

  2. Inclusive Boundaries: Points on the perimeter of rectangles are considered inside the rectangle

  3. Uniform Distribution: Every integer point within any rectangle should have an equal probability of being selected

You need to implement a Solution class with two methods:

  • __init__(rects): Initialize the data structure with the given rectangles
  • pick(): Return a random integer point [u, v] that lies inside one of the rectangles

Example: If you have a rectangle [1, 1, 3, 3], the valid integer points would be:

  • (1,1), (1,2), (1,3)
  • (2,1), (2,2), (2,3)
  • (3,1), (3,2), (3,3)

That's 9 total points (a 3×3 grid), and each should have a 1/9 probability of being selected when this is the only rectangle.

The challenge is handling multiple rectangles of different sizes while maintaining uniform probability across all valid points in all rectangles.

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

Intuition

The key insight is that we need to ensure every integer point across all rectangles has an equal probability of being selected. If we simply picked a random rectangle first and then a random point within it, larger rectangles would have their points selected less frequently than smaller rectangles.

Consider two rectangles: one with 100 integer points and another with just 10 integer points. If we pick each rectangle with 50% probability, then each point in the small rectangle has a 5% chance of being selected (50% × 1/10), while each point in the large rectangle only has a 0.5% chance (50% × 1/100). This violates the uniform distribution requirement.

The solution is to use weighted random selection based on the number of integer points in each rectangle. We need to:

  1. Calculate how many integer points each rectangle contains. For a rectangle [x1, y1, x2, y2], the number of integer points is (x2 - x1 + 1) × (y2 - y1 + 1). The +1 is crucial because the boundaries are inclusive.

  2. Build a cumulative sum array where s[i] represents the total number of integer points in rectangles 0 through i. This creates ranges where rectangle i "owns" the integers from s[i-1] + 1 to s[i].

  3. To pick a point, we generate a random number between 1 and the total number of points across all rectangles. We then use binary search to find which rectangle this number falls into based on our cumulative sum array.

  4. Once we know which rectangle to use, we can uniformly pick a random integer point within that specific rectangle using random.randint(x1, x2) and random.randint(y1, y2).

This approach ensures that if there are 110 total integer points (100 in the first rectangle and 10 in the second), each individual point has exactly a 1/110 probability of being selected, regardless of which rectangle it belongs to.

Solution Approach

The implementation uses a prefix sum array combined with binary search to efficiently select rectangles based on their point counts.

Initialization (__init__ method):

  1. Store the rectangles array for later use: self.rects = rects

  2. Create a prefix sum array self.s with the same length as the number of rectangles. Each element s[i] will store the cumulative count of all integer points from rectangle 0 to rectangle i.

  3. For each rectangle [x1, y1, x2, y2]:

    • Calculate the number of integer points: (x2 - x1 + 1) * (y2 - y1 + 1)
    • Add this count to the previous cumulative sum: self.s[i] = self.s[i - 1] + point_count
    • Note: When i = 0, self.s[i - 1] gives us self.s[-1] which Python interprets as the last element, but since we initialized with zeros, this works correctly

Pick Method (pick):

  1. Generate a random integer v between 1 and the total number of points (stored in self.s[-1]). This represents selecting the v-th point among all points.

  2. Use bisect_left(self.s, v) to find which rectangle contains the v-th point:

    • bisect_left returns the leftmost position where v could be inserted to keep the array sorted
    • This gives us the index of the rectangle that "owns" this point number
    • For example, if s = [0, 9, 19, 25] and v = 15, bisect_left returns index 2, meaning the point is in rectangle 2
  3. Retrieve the selected rectangle's coordinates: x1, y1, x2, y2 = self.rects[idx]

  4. Uniformly select a random integer point within this rectangle:

    • Random x-coordinate: random.randint(x1, x2)
    • Random y-coordinate: random.randint(y1, y2)
    • Return [x, y]

Time Complexity:

  • Initialization: O(n) where n is the number of rectangles
  • Pick: O(log n) for binary search plus O(1) for random point generation

Space Complexity:

  • O(n) to store the rectangles and prefix sum array

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example with three rectangles to illustrate how the solution works:

Given rectangles:

  • Rectangle 0: [1, 1, 2, 3]
  • Rectangle 1: [3, 2, 5, 3]
  • Rectangle 2: [2, 5, 3, 5]

Step 1: Calculate integer points in each rectangle

For Rectangle 0 [1, 1, 2, 3]:

  • Width: 2 - 1 + 1 = 2 points
  • Height: 3 - 1 + 1 = 3 points
  • Total points: 2 × 3 = 6 points
  • These are: (1,1), (1,2), (1,3), (2,1), (2,2), (2,3)

For Rectangle 1 [3, 2, 5, 3]:

  • Width: 5 - 3 + 1 = 3 points
  • Height: 3 - 2 + 1 = 2 points
  • Total points: 3 × 2 = 6 points
  • These are: (3,2), (3,3), (4,2), (4,3), (5,2), (5,3)

For Rectangle 2 [2, 5, 3, 5]:

  • Width: 3 - 2 + 1 = 2 points
  • Height: 5 - 5 + 1 = 1 point
  • Total points: 2 × 1 = 2 points
  • These are: (2,5), (3,5)

Step 2: Build prefix sum array

Starting with s = [0, 0, 0]:

  • s[0] = 0 + 6 = 6 (Rectangle 0 has 6 points)
  • s[1] = 6 + 6 = 12 (Rectangle 0 + Rectangle 1 = 12 points)
  • s[2] = 12 + 2 = 14 (All rectangles = 14 points total)

Final prefix sum array: s = [6, 12, 14]

This means:

  • Points 1-6 belong to Rectangle 0
  • Points 7-12 belong to Rectangle 1
  • Points 13-14 belong to Rectangle 2

Step 3: Simulate picking a random point

Let's say random.randint(1, 14) returns 10.

Using bisect_left(s, 10):

  • We search for where 10 would fit in [6, 12, 14]
  • Since 10 > 6 but 10 ≤ 12, bisect_left returns index 1
  • This means we select Rectangle 1: [3, 2, 5, 3]

Now we pick a random point within Rectangle 1:

  • Random x: random.randint(3, 5) → let's say it returns 4
  • Random y: random.randint(2, 3) → let's say it returns 3
  • Return [4, 3]

Probability verification: Each of the 14 integer points has exactly 1/14 probability of being selected:

  • Rectangle 0's 6 points: Each has probability (6/14) × (1/6) = 1/14
  • Rectangle 1's 6 points: Each has probability (6/14) × (1/6) = 1/14
  • Rectangle 2's 2 points: Each has probability (2/14) × (1/2) = 1/14

The weighted selection ensures uniform distribution across all points regardless of which rectangle they belong to.

Solution Implementation

1from typing import List
2import random
3from bisect import bisect_left
4
5
6class Solution:
7    def __init__(self, rects: List[List[int]]):
8        """
9        Initialize with a list of non-overlapping rectangles.
10        Each rectangle is represented as [x1, y1, x2, y2] where
11        (x1, y1) is bottom-left corner and (x2, y2) is top-right corner.
12        """
13        self.rects = rects
14        # Create prefix sum array to store cumulative count of points
15        self.prefix_sums = [0] * len(rects)
16      
17        for i, (x1, y1, x2, y2) in enumerate(rects):
18            # Calculate number of integer points in current rectangle
19            points_count = (x2 - x1 + 1) * (y2 - y1 + 1)
20          
21            # Build prefix sum: add current rectangle's points to previous sum
22            if i == 0:
23                self.prefix_sums[i] = points_count
24            else:
25                self.prefix_sums[i] = self.prefix_sums[i - 1] + points_count
26
27    def pick(self) -> List[int]:
28        """
29        Pick a random integer point from one of the rectangles.
30        Returns [x, y] coordinates of the selected point.
31        """
32        # Pick a random point index from 1 to total number of points
33        random_point_index = random.randint(1, self.prefix_sums[-1])
34      
35        # Find which rectangle contains this point using binary search
36        rectangle_index = bisect_left(self.prefix_sums, random_point_index)
37      
38        # Handle edge case where bisect_left returns exact match
39        if rectangle_index < len(self.prefix_sums) and self.prefix_sums[rectangle_index] < random_point_index:
40            rectangle_index += 1
41          
42        # Get the selected rectangle's coordinates
43        x1, y1, x2, y2 = self.rects[rectangle_index]
44      
45        # Pick a random point within the selected rectangle
46        random_x = random.randint(x1, x2)
47        random_y = random.randint(y1, y2)
48      
49        return [random_x, random_y]
50
51
52# Your Solution object will be instantiated and called as such:
53# obj = Solution(rects)
54# param_1 = obj.pick()
55
1class Solution {
2    // Prefix sum array to store cumulative point counts
3    private int[] prefixSum;
4    // Store the input rectangles
5    private int[][] rectangles;
6    // Random number generator
7    private Random random = new Random();
8
9    /**
10     * Constructor initializes the data structure with given rectangles.
11     * Calculates prefix sum of total points in each rectangle.
12     * 
13     * @param rects Array of rectangles, each defined as [x1, y1, x2, y2]
14     */
15    public Solution(int[][] rects) {
16        int numRectangles = rects.length;
17        prefixSum = new int[numRectangles + 1];
18      
19        // Calculate prefix sum of points in each rectangle
20        for (int i = 0; i < numRectangles; i++) {
21            // Calculate number of integer points in rectangle i
22            int width = rects[i][2] - rects[i][0] + 1;
23            int height = rects[i][3] - rects[i][1] + 1;
24            int pointsInRectangle = width * height;
25          
26            // Add to prefix sum
27            prefixSum[i + 1] = prefixSum[i] + pointsInRectangle;
28        }
29      
30        this.rectangles = rects;
31    }
32
33    /**
34     * Picks a random integer point from one of the rectangles.
35     * Each point has equal probability of being selected.
36     * 
37     * @return Array containing [x, y] coordinates of the selected point
38     */
39    public int[] pick() {
40        int numRectangles = rectangles.length;
41      
42        // Generate random number from 1 to total number of points
43        int randomPointIndex = 1 + random.nextInt(prefixSum[numRectangles]);
44      
45        // Binary search to find which rectangle contains this point
46        int left = 0;
47        int right = numRectangles;
48      
49        while (left < right) {
50            int mid = (left + right) >> 1;  // Equivalent to (left + right) / 2
51          
52            if (prefixSum[mid] >= randomPointIndex) {
53                right = mid;
54            } else {
55                left = mid + 1;
56            }
57        }
58      
59        // Get the selected rectangle (left - 1 because of 1-based indexing in prefix sum)
60        int[] selectedRectangle = rectangles[left - 1];
61      
62        // Generate random point within the selected rectangle
63        int randomX = selectedRectangle[0] + random.nextInt(selectedRectangle[2] - selectedRectangle[0] + 1);
64        int randomY = selectedRectangle[1] + random.nextInt(selectedRectangle[3] - selectedRectangle[1] + 1);
65      
66        return new int[] {randomX, randomY};
67    }
68}
69
70/**
71 * Your Solution object will be instantiated and called as such:
72 * Solution obj = new Solution(rects);
73 * int[] param_1 = obj.pick();
74 */
75
1class Solution {
2public:
3    vector<int> prefixSum;           // Cumulative sum of areas for weighted random selection
4    vector<vector<int>> rectangles;  // Store the input rectangles
5  
6    Solution(vector<vector<int>>& rects) {
7        int numRects = rects.size();
8        prefixSum.resize(numRects + 1);
9      
10        // Build prefix sum array where each element represents
11        // the cumulative area up to that rectangle
12        for (int i = 0; i < numRects; ++i) {
13            int width = rects[i][2] - rects[i][0] + 1;   // x2 - x1 + 1 (inclusive)
14            int height = rects[i][3] - rects[i][1] + 1;  // y2 - y1 + 1 (inclusive)
15            int area = width * height;
16            prefixSum[i + 1] = prefixSum[i] + area;
17        }
18      
19        this->rectangles = rects;
20        srand(time(nullptr));  // Initialize random seed
21    }
22  
23    vector<int> pick() {
24        int numRects = rectangles.size();
25      
26        // Generate random value in range [1, totalArea]
27        int randomValue = 1 + rand() % prefixSum[numRects];
28      
29        // Binary search to find which rectangle this random value falls into
30        int rectIndex = lower_bound(prefixSum.begin(), prefixSum.end(), randomValue) - prefixSum.begin();
31      
32        // Get the selected rectangle (adjust index by -1 since prefixSum is 1-indexed)
33        auto& selectedRect = rectangles[rectIndex - 1];
34      
35        // Generate random point within the selected rectangle
36        int randomX = selectedRect[0] + rand() % (selectedRect[2] - selectedRect[0] + 1);
37        int randomY = selectedRect[1] + rand() % (selectedRect[3] - selectedRect[1] + 1);
38      
39        return {randomX, randomY};
40    }
41};
42
43/**
44 * Your Solution object will be instantiated and called as such:
45 * Solution* obj = new Solution(rects);
46 * vector<int> param_1 = obj->pick();
47 */
48
1let prefixSum: number[] = [];           // Cumulative sum of areas for weighted random selection
2let rectangles: number[][] = [];        // Store the input rectangles
3
4function Solution(rects: number[][]): void {
5    const numRects = rects.length;
6    prefixSum = new Array(numRects + 1).fill(0);
7  
8    // Build prefix sum array where each element represents
9    // the cumulative area up to that rectangle
10    for (let i = 0; i < numRects; i++) {
11        const width = rects[i][2] - rects[i][0] + 1;   // x2 - x1 + 1 (inclusive)
12        const height = rects[i][3] - rects[i][1] + 1;  // y2 - y1 + 1 (inclusive)
13        const area = width * height;
14        prefixSum[i + 1] = prefixSum[i] + area;
15    }
16  
17    rectangles = rects;
18}
19
20function pick(): number[] {
21    const numRects = rectangles.length;
22  
23    // Generate random value in range [1, totalArea]
24    const totalArea = prefixSum[numRects];
25    const randomValue = Math.floor(Math.random() * totalArea) + 1;
26  
27    // Binary search to find which rectangle this random value falls into
28    let left = 0;
29    let right = prefixSum.length - 1;
30    while (left < right) {
31        const mid = Math.floor((left + right) / 2);
32        if (prefixSum[mid] < randomValue) {
33            left = mid + 1;
34        } else {
35            right = mid;
36        }
37    }
38    const rectIndex = left;
39  
40    // Get the selected rectangle (adjust index by -1 since prefixSum is 1-indexed)
41    const selectedRect = rectangles[rectIndex - 1];
42  
43    // Generate random point within the selected rectangle
44    const randomX = selectedRect[0] + Math.floor(Math.random() * (selectedRect[2] - selectedRect[0] + 1));
45    const randomY = selectedRect[1] + Math.floor(Math.random() * (selectedRect[3] - selectedRect[1] + 1));
46  
47    return [randomX, randomY];
48}
49
50/**
51 * Your Solution object will be instantiated and called as such:
52 * Solution(rects);
53 * const param_1 = pick();
54 */
55

Time and Space Complexity

Time Complexity:

  • __init__: O(n) where n is the number of rectangles. The method iterates through all rectangles once to calculate the cumulative area sum.
  • pick: O(log n) for the binary search using bisect_left to find the target rectangle, plus O(1) for generating random coordinates within the selected rectangle.

Space Complexity:

  • O(n) where n is the number of rectangles. The solution stores:
    • self.rects: O(n) space to store the original rectangles
    • self.s: O(n) space to store the cumulative sum array of areas

The algorithm uses a weighted random selection approach where rectangles with larger areas have proportionally higher chances of being selected. The cumulative sum array enables efficient rectangle selection using binary search.

Common Pitfalls

1. Incorrect Binary Search Logic

The most common pitfall in this solution is misunderstanding how bisect_left works with the prefix sum array and random point index.

The Problem: The code has a critical bug in the pick method. After using bisect_left, there's unnecessary logic trying to "fix" the rectangle index:

if rectangle_index < len(self.prefix_sums) and self.prefix_sums[rectangle_index] < random_point_index:
    rectangle_index += 1

This is incorrect because:

  • When random_point_index is between two prefix sum values, bisect_left returns the index of the larger value
  • The additional adjustment can cause index out of bounds errors or select the wrong rectangle

The Fix: Use bisect_right (or bisect) instead, which naturally gives us the correct rectangle index:

def pick(self) -> List[int]:
    # Pick a random point index from 1 to total number of points
    random_point_index = random.randint(1, self.prefix_sums[-1])
  
    # Find which rectangle contains this point using binary search
    rectangle_index = bisect.bisect_right(self.prefix_sums, random_point_index - 1)
  
    # Get the selected rectangle's coordinates
    x1, y1, x2, y2 = self.rects[rectangle_index]
  
    # Pick a random point within the selected rectangle
    random_x = random.randint(x1, x2)
    random_y = random.randint(y1, y2)
  
    return [random_x, random_y]

2. Off-by-One Error in Point Counting

Developers often forget that both boundaries are inclusive when counting integer points.

The Problem: Incorrectly calculating points as (x2 - x1) * (y2 - y1) instead of (x2 - x1 + 1) * (y2 - y1 + 1).

Example: For rectangle [1, 1, 3, 3]:

  • Wrong: (3-1) * (3-1) = 4 points
  • Correct: (3-1+1) * (3-1+1) = 9 points

3. Random Range Selection Error

Using 0-based indexing for the random point selection instead of 1-based.

The Problem: Using random.randint(0, self.prefix_sums[-1] - 1) can cause issues with the binary search logic since the prefix sum array conceptually represents cumulative counts starting from 1.

The Fix: Stick with 1-based indexing: random.randint(1, self.prefix_sums[-1]) and adjust the binary search accordingly.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Which of the following array represent a max heap?


Recommended Readings

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

Load More