LeetCode 149. 直线上最多的点数
题目描述


题意分析
给定平面上互不相同的点,求一条直线最多经过多少个点。点的输入顺序不代表几何位置,同一条直线可以从基准点向两个相反方向延伸。
若先选两个点确定直线,再扫描所有点判断是否在线上,需要三层枚举。固定一个基准点后,只要把其他点按经过基准点的直线分组,就能一次统计所有候选直线。
解法:枚举基准点 + 方向向量规范化
核心思路
[!blue]
固定点
i,点j相对它的方向向量为(dx, dy)。经过同一基准点的两个向量只要成比例,对应的点就在同一条直线上。直接比较dy / dx会遇到竖直线除零和浮点精度问题,因此用整数对保存比例。先求
gcd(abs(dx), abs(dy)),再把两个分量同时除以它,消除长度差异。约分后仍可能有正反两个表示,所以规定dx必须为正;若dx == 0,则让dy为正。这样同一条线的所有向量都有唯一键,水平线与竖直线也分别统一为(1, 0)和(0, 1)。
count[direction]表示已经扫描到、与基准点形成该方向的其他点数。它不包含基准点,因此该直线经过的点数是count[direction] + 1。哈希表必须随基准点重新创建,因为同一方向经过不同基准点时,可能代表不同的平行线。内层只枚举
j > i。任取一条最优直线,其中下标最小的点作为基准点时,线上其余点都在它后面,并且会落到同一个键中。因此这样的枚举一定能完整统计最优直线,不需要同时扫描前面的点。
解题步骤
- 枚举基准点
i,为它创建方向计数表。- 枚举
j > i,计算dx = xj - xi、dy = yj - yi。- 用最大公约数约分
dx、dy。- 若
dx < 0,或dx == 0 && dy < 0,同时翻转二者符号。- 将规范化后的整数对作为哈希键并累加计数,用“计数 + 1”更新答案。
题目保证至少有一个点,所以答案从
1开始;只有一个点时内层循环不会执行。所有点互不相同,保证dx、dy不会同时为零,最大公约数可以安全用于约分。坐标绝对值不超过 $10^4$,坐标差及其绝对值都在int范围内。
代码实现
class Solution {
public int maxPoints(int[][] points) {
int answer = 1;
for (int i = 0; i < points.length; i++) {
Map<String, Integer> count = new HashMap<>();
for (int j = i + 1; j < points.length; j++) {
int dx = points[j][0] - points[i][0];
int dy = points[j][1] - points[i][1];
// 用最大公约数约分方向,保持整数比较。
int divisor = gcd(Math.abs(dx), Math.abs(dy));
dx /= divisor;
dy /= divisor;
// 把相反向量也规范成同一方向,竖直方向单独统一符号。
if (dx < 0 || (dx == 0 && dy < 0)) {
dx = -dx;
dy = -dy;
}
String direction = dx + "," + dy;
int sameDirection = count.getOrDefault(direction, 0) + 1;
count.put(direction, sameDirection);
// 方向计数不含基准点,答案还要加上它。
answer = Math.max(answer, sameDirection + 1);
}
}
return answer;
}
private int gcd(int a, int b) {
while (b != 0) {
int remainder = a % b;
a = b;
b = remainder;
}
return a;
}
}
func maxPoints(points [][]int) int {
answer := 1
for i := 0; i < len(points); i++ {
count := make(map[[2]int]int)
for j := i + 1; j < len(points); j++ {
dx := points[j][0] - points[i][0]
dy := points[j][1] - points[i][1]
// 用最大公约数约分方向,保持整数比较。
divisor := gcd(abs(dx), abs(dy))
dx /= divisor
dy /= divisor
// 把相反向量也规范成同一方向,竖直方向单独统一符号。
if dx < 0 || dx == 0 && dy < 0 {
dx = -dx
dy = -dy
}
direction := [2]int{
dx,
dy,
}
count[direction]++
// 方向计数不含基准点,答案还要加上它。
if count[direction]+1 > answer {
answer = count[direction] + 1
}
}
}
return answer
}
func gcd(a, b int) int {
for b != 0 {
a, b = b, a%b
}
return a
}
func abs(value int) int {
if value < 0 {
return -value
}
return value
}
复杂度分析
- 时间复杂度:$O(n^2 \log C)$,其中 $C$ 是坐标差的最大值;每对点计算一次最大公约数。坐标范围固定时通常记为 $O(n^2)$。
- 空间复杂度:$O(n)$,固定一个基准点时,哈希表最多保存 $n-1$ 个方向。
关键点总结
[!green]
- 枚举基准点后,二维共线问题转化为方向分组计数。
- 用约分后的整数对表示方向,避免浮点精度和竖直线除零问题。
- 最大公约数只消除倍数,符号规范化负责合并相反向量。
- 哈希表不含基准点自身,因此局部计数要加 1。
易错点总结
[!yellow]
- 使用
double斜率可能把不同方向误判为相同,也必须额外处理dx == 0。- 只做最大公约数约分、不统一符号,会把
(1,1)和(-1,-1)分开。- 只统一
dx < 0而漏掉竖直方向,会把(0,1)、(0,-1)分开。- 局部计数不含基准点,答案要加一;每换基准点必须新建计数表,不能混用不同直线。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 面试题 16.14. 最佳直线 | 中等 | 同样按锚点统计精确方向,原题还返回最早的两个下标并处理并列规则。 |
| 1232. 缀点成线 | 简单 | 原题只检查所有点是否同线,本题需要在多个候选方向中选最多的一组。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!