LeetCode 497. 非重叠矩形中的随机点
题目描述



题意分析
给定若干互不重叠、边平行于坐标轴的矩形,每次从它们覆盖的所有整数坐标点中等概率返回一个点,边界上的点也算在内。等概率针对每个点,而不是每个矩形。
解法:格点前缀计数 + 二分采样
核心思路
[!blue]
为所有格点分配从 1 到
total的连续编号,再均匀抽一个编号。矩形[x1, y1, x2, y2]横向有x2 - x1 + 1个整数坐标,纵向有y2 - y1 + 1个,所以包含两者乘积个格点。矩形互不重叠,所有格点都能获得唯一编号。用
prefix[i]保存前i + 1个矩形的格点总数。第i个矩形占据编号prefix[i - 1] + 1到prefix[i],第一个矩形从 1 开始。随机取得k后,二分第一个prefix[i] >= k的位置,就找到了它所属的矩形;较大矩形占据更多编号,自然会按格点数获得更高的选中概率。设此前矩形共有
base个点,当前矩形内的零起始偏移为offset = k - base - 1。按列排列格点,每列有height个点,所以offset / height是第几列,offset % height是列内第几个点;加上左下角坐标即可还原结果。这个商余映射在矩形内部是一一对应的,且所有编号被均匀抽取,因此每个格点恰好以
1 / total的概率返回,无需分别再随机选择横、纵坐标。
解题步骤
- 构造时累计每个矩形的格点数,保存 64 位前缀和
prefix和总点数total。- 调用有界整数随机接口,在
[1, total]中均匀取得编号k。- 二分第一个前缀和不小于
k的下标;中点满足条件就向左收缩,否则排除中点及左侧。- 计算此前点数
base和内部偏移offset = k - base - 1。- 返回
x1 + offset / height、y1 + offset % height。
代码实现
class Solution {
private final int[][] rects;
private final long[] prefix;
private final long total;
public Solution(int[][] rects) {
this.rects = rects;
this.prefix = new long[rects.length];
long count = 0;
for (int i = 0; i < rects.length; i++) {
int[] r = rects[i];
long width = (long) r[2] - r[0] + 1;
long height = (long) r[3] - r[1] + 1;
count += width * height;
prefix[i] = count;
}
this.total = count;
}
public int[] pick() {
// 在全部格点编号中均匀抽样,矩形概率自然与点数成正比。
long k = java.util.concurrent.ThreadLocalRandom.current().nextLong(total) + 1;
int idx = lowerBound(prefix, k);
int[] r = rects[idx];
long height = (long) r[3] - r[1] + 1;
long base = idx == 0 ? 0 : prefix[idx - 1];
// 整体编号从一开始,矩形内部偏移转换为从零开始。
long offset = k - base - 1;
// 按列展开:商是横向偏移,余数是纵向偏移。
int x = (int) (r[0] + offset / height);
int y = (int) (r[1] + offset % height);
return new int[] {
x,
y
};
}
private int lowerBound(long[] arr, long target) {
int l = 0;
int r = arr.length;
while (l < r) {
int m = l + (r - l) / 2;
if (arr[m] >= target) {
r = m;
} else {
l = m + 1;
}
}
return l;
}
}
import "math/rand"
type Solution struct {
rects [][]int
prefix []int64
total int64
}
func Constructor(rects [][]int) Solution {
prefix := make([]int64, len(rects))
var total int64
for i, r := range rects {
width := int64(r[2]) - int64(r[0]) + 1
height := int64(r[3]) - int64(r[1]) + 1
total += width * height
prefix[i] = total
}
return Solution{rects: rects, prefix: prefix, total: total}
}
func (this *Solution) Pick() []int {
// 在全部格点编号中均匀抽样,矩形概率自然与点数成正比。
k := rand.Int63n(this.total) + 1
idx := lowerBound(this.prefix, k)
r := this.rects[idx]
height := int64(r[3]) - int64(r[1]) + 1
var base int64
if idx > 0 {
base = this.prefix[idx-1]
}
// 整体编号从一开始,矩形内部偏移转换为从零开始。
offset := k - base - 1
// 按列展开:商是横向偏移,余数是纵向偏移。
x := r[0] + int(offset/height)
y := r[1] + int(offset%height)
return []int{
x,
y,
}
}
func lowerBound(arr []int64, target int64) int {
l, r := 0, len(arr)
for l < r {
m := l + (r-l)/2
if arr[m] >= target {
r = m
} else {
l = m + 1
}
}
return l
}
复杂度分析
- 时间复杂度:构造为 $O(R)$,单次采样期望为 $O(\log(R+1))$,其中
R为矩形数;随机编号后只需一次二分及常数次坐标计算。- 空间复杂度:$O(R)$,保存前缀点数,不展开全部格点。
关键点总结
[!green]
- 均匀的是最终格点,不是矩形编号。
- 闭区间每个方向都要加一。
- 随机编号、下界比较与内部偏移必须使用同一套起始约定。
易错点总结
[!yellow]
- 等概率选矩形再选点:不同大小矩形中的点概率不同。
- 点数漏掉加一:单点或线段矩形可能被算成零容量。
- 下界使用严格大于:矩形最后一个编号归属错误。
- 除数与坐标方向混用:商和余数可能产生越界坐标。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 528. 按权重随机选择 | 中等 | 先按每个矩形包含的整数点数量加权选择区域,再在区域内均匀选点。 |
| 478. 在圆内随机生成点 | 中等 | 本题是离散格点,矩形点数包含边界所以边长需加1;圆内采样则针对连续面积。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!