LeetCode 497 - Random Point in Non-overlapping Rectangles
The problem asks us to randomly pick an integer point from a set of non-overlapping rectangles in 2D space. Each rectangle is defined by its bottom-left (ai, bi) and top-right (xi, yi) corners.
Difficulty: 🟡 Medium
Topics: Array, Math, Binary Search, Reservoir Sampling, Prefix Sum, Ordered Set, Randomized
Solution
Problem Understanding
The problem asks us to randomly pick an integer point from a set of non-overlapping rectangles in 2D space. Each rectangle is defined by its bottom-left (ai, bi) and top-right (xi, yi) corners. The goal is to ensure that every integer point inside the union of all rectangles has an equal probability of being selected. A point on the edge of a rectangle counts as part of the rectangle.
The input rects is a list of rectangles, each represented by four integers. The output is a randomly selected integer point [u, v] inside one of the rectangles. The constraints tell us there can be up to 100 rectangles, each with side lengths up to 2000, and the coordinates can be very large (up to 10^9 in magnitude). We may call the pick() function up to 10^4 times.
Important edge cases include rectangles with minimal area (like 1x1), rectangles that share edges but not overlap, and negative coordinates. The problem guarantees no overlap, so we do not have to handle overlapping areas.
Approaches
A brute-force solution would enumerate all integer points in all rectangles, store them in a list, and pick a random element from the list. This guarantees uniformity because each point appears exactly once. However, this approach is infeasible because the total number of points can reach 100 * 2001 * 2001, which is over 400 million points. The space and time complexity are too high for this input range.
The key insight for an optimal solution is that we do not need to enumerate every point. Instead, we can compute the number of points in each rectangle, maintain a prefix sum of these counts, and use it to pick a rectangle with probability proportional to its area. Once a rectangle is chosen, we can randomly select a point inside it using simple arithmetic. This reduces both memory usage and runtime drastically while preserving uniformity.
| Approach | Time Complexity | Space Complexity | Notes |
|---|---|---|---|
| Brute Force | O(total_points) | O(total_points) | Enumerates all integer points |
| Optimal | O(log n) per pick | O(n) | Uses prefix sum and binary search over rectangles |
Algorithm Walkthrough
- Initialization: Compute the number of integer points in each rectangle. For rectangle
[ai, bi, xi, yi], the number of points is(xi - ai + 1) * (yi - bi + 1). Store the cumulative sum of these counts in a listprefix_sum. - Random Rectangle Selection: Generate a random integer
kbetween1and the total number of points. Use binary search on theprefix_sumarray to find which rectangle contains thek-th point. - Random Point in Rectangle: Once a rectangle
[ai, bi, xi, yi]is selected, generate a random integerdxin[0, xi - ai]anddyin[0, yi - bi]. The chosen point is(ai + dx, bi + dy). - Return Result: Return the generated point as a list of two integers.
Why it works: The prefix sum ensures each rectangle is chosen with probability proportional to the number of integer points it contains. Picking a point uniformly within the rectangle guarantees each point is equally likely.
Python Solution
import random
from typing import List
class Solution:
def __init__(self, rects: List[List[int]]):
self.rects = rects
self.prefix_sum = []
total = 0
for x1, y1, x2, y2 in rects:
points = (x2 - x1 + 1) * (y2 - y1 + 1)
total += points
self.prefix_sum.append(total)
def pick(self) -> List[int]:
k = random.randint(1, self.prefix_sum[-1])
# Binary search to find the rectangle
low, high = 0, len(self.prefix_sum) - 1
while low < high:
mid = (low + high) // 2
if k <= self.prefix_sum[mid]:
high = mid
else:
low = mid + 1
rect_index = low
x1, y1, x2, y2 = self.rects[rect_index]
dx = random.randint(0, x2 - x1)
dy = random.randint(0, y2 - y1)
return [x1 + dx, y1 + dy]
The Python implementation first precomputes the prefix sum of points in each rectangle. Picking a random point involves selecting a rectangle with binary search and generating a random point inside it. This avoids generating all points, providing an efficient O(log n) pick time.
Go Solution
package main
import (
"math/rand"
"time"
)
type Solution struct {
rects [][]int
prefixSum []int
}
func Constructor(rects [][]int) Solution {
rand.Seed(time.Now().UnixNano())
prefix := make([]int, len(rects))
total := 0
for i, rect := range rects {
x1, y1, x2, y2 := rect[0], rect[1], rect[2], rect[3]
points := (x2 - x1 + 1) * (y2 - y1 + 1)
total += points
prefix[i] = total
}
return Solution{
rects: rects,
prefixSum: prefix,
}
}
func (this *Solution) Pick() []int {
totalPoints := this.prefixSum[len(this.prefixSum)-1]
k := rand.Intn(totalPoints) + 1
// Binary search
low, high := 0, len(this.prefixSum)-1
for low < high {
mid := (low + high) / 2
if k <= this.prefixSum[mid] {
high = mid
} else {
low = mid + 1
}
}
rect := this.rects[low]
x1, y1, x2, y2 := rect[0], rect[1], rect[2], rect[3]
dx := rand.Intn(x2 - x1 + 1)
dy := rand.Intn(y2 - y1 + 1)
return []int{x1 + dx, y1 + dy}
}
The Go version mirrors Python closely. Differences include seeding the random number generator and using rand.Intn(n) which generates values [0, n). The +1 adjustments ensure inclusive ranges.
Worked Examples
Example Input: [[-2, -2, 1, 1], [2, 2, 4, 6]]
- Compute points: first rectangle
(1-(-2)+1)*(1-(-2)+1) = 4*4=16, second rectangle(4-2+1)*(6-2+1)=3*5=15. Prefix sum[16, 31]. - Pick a random
kbetween 1 and 31. Supposek = 18. - Binary search on
[16, 31]gives rectangle 1 (second rectangle). - Random point in second rectangle: dx = rand(0,2), dy = rand(0,4). Suppose dx=1, dy=2. Result
[2+1,2+2]=[3,4].
Complexity Analysis
| Measure | Complexity | Explanation |
|---|---|---|
| Time | O(n) init, O(log n) per pick | Init computes prefix sum; pick uses binary search |
| Space | O(n) | Stores prefix sum array |
The pick operation does not depend on rectangle size, only the number of rectangles, which is efficient even for large rectangles.
Test Cases
# Example tests
sol = Solution([[-2, -2, 1, 1], [2, 2, 4, 6]])
for _ in range(5):
point = sol.pick()
assert len(point) == 2 # point has two coordinates
x, y = point
assert (-2 <= x <= 1 and -2 <= y <= 1) or (2 <= x <= 4 and 2 <= y <= 6)
# Edge case: single rectangle of size 1x1
sol = Solution([[0, 0, 0, 0]])
assert sol.pick() == [0, 0]
# Edge case: negative coordinates only
sol = Solution([[-5, -5, -1, -1]])
for _ in range(10):
x, y = sol.pick()
assert -5 <= x <= -1 and -5 <= y <= -1
# Multiple rectangles, varying sizes
sol = Solution([[0,0,1,1],[10,10,12,12],[5,5,5,5]])
for _ in range(10):
x, y = sol.pick()
assert (0 <= x <= 1 and 0 <= y <= 1) or (10 <= x <= 12 and 10 <= y <= 12) or (x == 5 and y == 5)
| Test | Why |
|---|---|
| Example rectangles | Validates correct probability distribution and bounds |
| Single 1x1 rectangle | Tests minimal rectangle handling |
| Negative coordinates | Ensures |