LeetCode 1610. 可见点的最大数目
题目描述




题意分析
观察者的位置固定,可以任意旋转一个宽度为
angle的视野,求最多能同时看见多少个点。视野边界上的点也算可见,点之间不会互相遮挡,所以距离远近不影响答案。同一个坐标可以有多条点记录,都要分别计数;与观察者重合的点在任何朝向下都可见。其余点只需要考虑相对观察者的方向。
解法:极角排序 + 滑动窗口
核心思路
[!blue]
先把与观察者重合的点计入
same,其他点用atan2(dy, dx)转成极角。它同时考虑横纵坐标的符号,能区分全部象限;同方向的多个点保留多个角度记录,不去重。将角度排序后,一个朝向下可见的方向在圆周上必然是连续一段。反过来,只要一段方向的最大跨度不超过视角,就一定能把视野旋转到覆盖它。因此问题转为寻找跨度受限的最长连续段。
圆周存在首尾边界,接近最小角度和最大角度的点也可能相邻。把排好序的角度复制一份,每项加
2π后接到原数组末尾,就把跨边界的视野展开成直线上的普通区间。题目视角小于一整周,两份角度已经能表示全部可能的圆周区间。用左右指针维护窗口,右端逐个扩展。当首尾角度差超过视角,或窗口点数超过原有方向点数
m时,持续右移左端。排序后扩展右端不会减小跨度,移动左端不会增大跨度,首次恢复合法时就是当前右端下的最长合法窗口。
atan2返回弧度,需要把输入角度也转换成弧度再比较。边界本应包含在视野内,代码加入很小的误差容忍,避免浮点舍入把恰好在边界上的点排除。窗口最大长度最后加上始终可见的same。
解题步骤
- 计算每个点相对观察者的坐标差,重合点单独计数,其余点转换为方向角。
- 对方向角排序。若没有非重合点,直接返回
same。- 创建长度为
2m的数组,前半保存原角度,后半保存原角度加2π。- 将
angle从角度制转为弧度,扫描右端点;窗口跨度超限或数量超过m时收缩左端。- 每次恢复合法后更新最大窗口长度,最后加上重合点数量返回。
代码实现
class Solution {
private static final double EPSILON = 1e-12;
public int visiblePoints(List<List<Integer>> points, int angle, List<Integer> location) {
List<Double> directions = new ArrayList<>();
int same = 0;
for (List<Integer> point : points) {
int dx = point.get(0) - location.get(0);
int dy = point.get(1) - location.get(1);
// 同位置点始终可见,不参与方向窗口。
if (dx == 0 && dy == 0) {
same++;
} else {
directions.add(Math.atan2(dy, dx));
}
}
Collections.sort(directions);
int m = directions.size();
if (m == 0) {
return same;
}
// 复制并加一周,将跨越角度边界的窗口展开到直线上。
double[] extended = new double[2 * m];
for (int i = 0; i < m; i++) {
extended[i] = directions.get(i);
extended[i + m] = directions.get(i) + 2 * Math.PI;
}
double limit = Math.toRadians(angle);
int left = 0;
int best = 0;
for (int right = 0; right < extended.length; right++) {
// 收缩到合法视角,同时不让窗口超过原方向点数。
while (right - left + 1 > m || extended[right] - extended[left] > limit + EPSILON) {
left++;
}
best = Math.max(best, right - left + 1);
}
return best + same;
}
}
import (
"math"
"sort"
)
const epsilon = 1e-12
func visiblePoints(points [][]int, angle int, location []int) int {
directions := make([]float64, 0, len(points))
same := 0
for _, point := range points {
dx := point[0] - location[0]
dy := point[1] - location[1]
// 同位置点始终可见,不参与方向窗口。
if dx == 0 && dy == 0 {
same++
} else {
directions = append(directions, math.Atan2(float64(dy), float64(dx)))
}
}
sort.Float64s(directions)
m := len(directions)
if m == 0 {
return same
}
// 复制并加一周,将跨越角度边界的窗口展开到直线上。
extended := make([]float64, 2*m)
for i, direction := range directions {
extended[i] = direction
extended[i+m] = direction + 2*math.Pi
}
limit := float64(angle) * math.Pi / 180
left, best := 0, 0
for right := range extended {
// 收缩到合法视角,同时不让窗口超过原方向点数。
for right-left+1 > m || extended[right]-extended[left] > limit+epsilon {
left++
}
if right-left+1 > best {
best = right - left + 1
}
}
return best + same
}
复杂度分析
- 时间复杂度:$O(n\log(n+1))$,排序占主导,窗口线性移动。
- 空间复杂度:$O(n)$,保存方向和扩展数组。
关键点总结
[!green]
- 位置重合的点不受方向限制,其余点只按方向判断可见性。
- 排序把视野内的点变成连续区间,复制并加一周处理跨越首尾的区间。
- 窗口最多包含
m个原始方向记录,扩展数组中的复制项不能让同一个点重复贡献。- 左右指针只向前移动,排序后的窗口扫描为线性时间。
易错点总结
[!yellow]
- 不能把重合点当作方向零,否则它们会被错误地限制在某些朝向内。
- 不能按角度或坐标去重,同位置、同方向的不同点记录都应保留计数。
- 只扫描原角度数组会漏掉跨首尾边界的视野;复制后仍要限制原始点的计数范围。
atan(dy / dx)不能区分全部象限,竖直方向还可能除零,应使用atan2(dy, dx)。- 比较双方必须使用相同角度单位,并保留闭区间边界及必要的浮点误差容忍。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 539. 最小时间差 | 中等 | 同样处理周期边界,可把排序后的角度复制并加一周,再在线性序列中处理跨零方向的窗口。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!