149. Max Points on a Line
给定一堆点,求最多有多少个点在同一直线上。
这道题被标记为Hard,而且AC率只有14.3%,刚看到是有点虚的。。。不过实际试了下之后,发现其实是道数学题。算法和数据结构上都没什么特殊的,算法只能是暴力O(N^2),数据结构只用到Map和Set。而且时间条件很宽松,我本来很担心TLE的,结果WA了几次后居然过了。
基本思路是用一个Map记录所有出现过的直线,和直线上已经有的点。两个嵌套for循环,每两个点计算出一条直线,看这条直线是否已经出现过。没出现过则丢到Map里,已出现过则更新。
要注意的问题:
- 如何表示一条直线?我是采用
y=kx+b
的形式,用两个double变量(k和b)唯一表示一条直线。 - 如何处理重复的点?
- 如何处理垂直于X轴的线(斜率k为正无穷)?
- 据说double会有精度问题,比如
+0.0
和-0.0
之类的,还有Double.NaN
,不过我没遇到。可能也是leetcode的test case没覆盖到。
代码:
public class Solution {
public int maxPoints(Point[] points) {
// 注意边界条件
if (points == null)
return 0;
if (points.length < 3)
return points.length;
int max = 0;
// 为了处理重复的点,我是先去重了一次
// WeightPoint的weight变量表示这个点出现了多少次
// 排序是O(NlogN),跟后面的O(N^2)相比应该不算什么吧
List<WeightPoint> weightPoints = sortAndUniq(points);
// 注意去重后要再判断一次边界条件
if (weightPoints.size() <= 2) {
for (WeightPoint p : weightPoints) {
max += p.weight;
}
return max;
}
// 用了一个很别扭的结构来保存每条直线的信息
// y=kx+b
// 第一个map的key是k,第二个map的key是b,value是当前这条直线上已经有哪些点,用一个Set去重
// 如果能用guava的Table就好了
Map<Double, Map<Double, Set<Integer>>> map = new HashMap<Double, Map<Double, Set<Integer>>>();
// 开始穷举,每两个点计算一次
for (int i = 0; i < weightPoints.size(); i++) {
for (int j = i + 1; j < weightPoints.size(); j++) {
Point a = weightPoints.get(i), b = weightPoints.get(j);
double x = calcSlope(a, b); // 计算斜率k
double y = 0; // 计算常数b
// 如果是垂直于x轴的直线,我会返回一个特殊值,这时b要取x坐标
if (x == Double.MIN_VALUE) {
y = a.x;
}
// 否则就按公式计算b
else {
y = a.y - x * a.x;
}
// 开始更新map
if (map.containsKey(x)) {
if (map.get(x).containsKey(y)) {
Set<Integer> set = map.get(x).get(y);
set.add(i);
set.add(j);
} else {
Set<Integer> set = new HashSet<Integer>();
set.add(i);
set.add(j);
map.get(x).put(y, set);
}
} else {
Set<Integer> set = new HashSet<Integer>();
set.add(i);
set.add(j);
Map<Double, Set<Integer>> tmp = new HashMap<Double, Set<Integer>>();
tmp.put(y, set);
map.put(x, tmp);
}
}
}
// 穷举完毕,开始计算max
for (Double d1 : map.keySet()) {
for (Double d2 : map.get(d1).keySet()) {
// 计算每条直线上有多少点
Set<Integer> set = map.get(d1).get(d2);
int tmp = 0;
for (Integer i : set) {
// 如果有重复的点,也要算进去
tmp += weightPoints.get(i).weight;
}
max = Math.max(max, tmp);
}
}
return max;
}
// 计算斜率,注意对垂直于x轴的直线的特殊处理
private double calcSlope(Point a, Point b) {
if (a.x == b.x)
return Double.MIN_VALUE;
else
return (double) (b.y - a.y) / (b.x - a.x);
}
// 判断两个点是否相同
private boolean samePoint(Point a, Point b) {
return a.x == b.x && a.y == b.y;
}
// 排序并且去重
private List<WeightPoint> sortAndUniq(Point[] points) {
Arrays.sort(points, new Comparator<Point>() {
@Override
public int compare(Point o1, Point o2) {
int tmp = Integer.compare(o1.x, o2.x);
if (tmp == 0)
return Integer.compare(o1.y, o2.y);
else
return tmp;
}
});
List<WeightPoint> res = new ArrayList<WeightPoint>();
Point last = null;
int count = 0;
for (int i = 0; i < points.length; i++) {
if (last == null) {
last = points[i];
count = 1;
continue;
}
if (samePoint(last, points[i])) {
count++;
continue;
}
WeightPoint newP = new WeightPoint();
newP.x = last.x;
newP.y = last.y;
newP.weight = count;
res.add(newP);
last = points[i];
count = 1;
}
WeightPoint newP = new WeightPoint();
newP.x = last.x;
newP.y = last.y;
newP.weight = count;
res.add(newP);
return res;
}
}
// 一个辅助类,多了一个weight字段,用于保存每个点的出现次数
class WeightPoint extends Point {
int weight = 1;
public String toString() {
return x + ":" + y + ":" + weight;
}
}
写完之后发现,我去这个解法怎么这么长。。。虽然能AC。但纯算法题写这么多代码,一般是有问题的。应该有更好的解法,哪天有心情再折腾吧。