原创,转载注明出处,最后修正日期 2019.2.15
能力一般,水平有限.还希望各位大牛们批评指正.
题目传送门:https://leetcode.com/problems/max-points-on-a-line/
这个是全leetcode上,通过率第二低的题,也是第二难的题。
在LeetCode上统计中,Alibaba的面试中和Baidu的面试中也出现过这个问题,不过你要是碰到这道题也算是面试官看得起你了,中奖了兄台。
回归正题:其实这道题没tm啥技术含量,就是巨麻烦。
在网上没有看到很好很完善的中文教程,于是打算自己写一个。
那么我们现在一步一步的来干掉她。
描述:
Given n points on a 2D plane, find the maximum number of points that lie on the same straight line.
Example 1:
Input: [[1,1],[2,2],[3,3]]
Output: 3
Explanation:
^
|
| o
| o
| o
+------------->
0 1 2 3 4
Example 2:
Input: [[1,1],[3,2],[5,3],[4,1],[2,3],[1,4]]
Output: 4
Explanation:
^
|
| o
| o o
| o
| o o
+------------------->
0 1 2 3 4 5 6
准备知识:
如果不准备下面这些知识,虽然这些知识很基础,但除非你数学基础好的一笔,否则下面不可能看懂的
求直线方程:
https://zh.wikihow.com/%E6%B1%82%E7%9B%B4%E7%BA%BF%E6%96%B9%E7%A8%8B
https://www.shuxuele.com/algebra/line-equation-point-slope.html
最大公约数(我们后面会用欧几里得法):
https://baike.baidu.com/item/%E6%9C%80%E5%A4%A7%E5%85%AC%E7%BA%A6%E6%95%B0
https://blog.csdn.net/Holmofy/article/details/76401074
解法1 以线为中心暴力求解(WrongAnswer):
这个解法现在是无法ac的,原因是乘法溢出,但是不失为一种好的,直接的方法。
testcase: [[0,0],[1,65536],[65536,0]] 乘法溢出
思路:
暴力枚举法。两点决定一条直线,n个点两两组合,可以得到(n(n+1))/2
直线,对每一条直线,判断n个点是否在该直线上,从而可以得到这条直线上的点的个数,选择最大的那条直线返回。复杂度O(n^3)
。
public int maxPoints(Point[] points) {
// 边界:一共两个点你还判断个鸡毛共线
final int len = points.length;
if (len < 3) {
return len;
}
int max = 0;
for (int i = 0; i < len - 1; ++i) {
for (int j = i + 1; j < len; ++j) {
// points[i] 和 points[j]
// 这里需要一个bool值的slope 来判断斜率是否存在,不存在的话设置为false,后续单独处理
boolean slope = true;
int dX = points[i].x - points[j].x;
int dY = points[i].y - points[j].y;
int interceptDX = 0;
if (dX == 0) {
// 这时候两个点连线是垂直于x轴的,木有斜率
slope = false;
} else {
// 这个是点斜式的变形, 等式左侧是(截距*dx),自己在演算纸上验算一下吧,就不详细说了
interceptDX = dX * points[i].y - dY * points[i].x;
}
int count = 0;
for (int k = 0; k < len; ++k) {
if (slope) {
// 将k点的x和y带入看直线方程是否有解。
if (interceptDX == dX * points[k].y - dY * points[k].x) {
++count;
}
} else {
if (points[k].x == points[i].x) {
++count;
}
}
}
max = Math.max(max, count);
}
}
return max;
}
很简单直接的解决办法,但是问题是会乘法溢出,这里有一个很特殊的地方,java没有无符号整数,而因为这道题的point的x和y的成员变量都是int类型,int类型的最大值是2^31-1
,long类型的最大值是2^63-1
,所以说不会存在溢出问题,然而在实际应用中是有无符号整数的可能的,所以:
这里如果将的dx
,dy
,ineterceptDX
几个变量类型全部改成long ,是可以通过的,结果是8ms,超过95%的java Solution.
解法2 以点为中心暴力求解(WrongAndswer)
这个解法现在是无法ac的,原因是除法精度问题,这个是我想到的第一种办法,但是无法ac,时间复杂度为O(n^2)
testcase [[0,0],[94911151,94911150],[94911152,94911151]] 精度不够。
public int maxPoints(Point[] points) {
// 同上
final int len = points.length;
if (len < 3) {
return len;
}
int max = 0;
// 创建一个hash表来存储斜率对应的点的数量。
Map map = new HashMap<>();
for (int i = 0; i <= len - 2; ++i) {
map.clear();
int samePoint = 0;// 与pi 重合的点
int sameLine = 1;// 和pi共线的最大点数
for (int j = i + 1; j <= len - 1; ++j) {
Double slope = null;
if (points[i].x == points[j].x) {
// 垂直与x轴的情况
slope = Double.POSITIVE_INFINITY;
// 重合的点
if (points[i].y == points[j].y) {
++samePoint;
continue;
}
} else {
if (points[i].y == points[j].y) {
// 这个涉及一个 -0 和 +0的问题, 在float和double里,+0是大于-0的,这个跟小数的实现有关系,有兴趣自行查一下
slope = 0.0;
} else {
// 求斜率
slope = 1.0 * (points[i].y - points[j].y) / (points[i].x - points[j].x);
}
}
Integer slopeCount = map.get(slope);
if (slopeCount != null) {
map.put(slope, slopeCount + 1);
slopeCount += 1;
} else {
slopeCount = 2;
map.put(slope, slopeCount);
}
sameLine = Math.max(sameLine, slopeCount);
}
// 共线的点加上重合的点
max = Math.max(max, sameLine + samePoint);
}
return max;
}
我层试过用BigDecimal来算,依然会有精度问题(至少我觉得最后导致失败的原因是精度问题)。
好的,那么问题来了,除法会有精度问题,我们现在就要去避免这个精度问题,我们可以用最大公约数来避免除法,把除数和被除数除以他俩的最大公约数,就是说避免查询double,而去查询两个Int来避免除法,非常优秀的想法。
在贴代码之前我们先贴一段最大公约数的证明。
不妨设A > B,设A和B的最大公约数为X,
所以 A=aX,B=bX,其中a和b都为正整数且a>b。
A除以B的余数: R = A - kB*,其中k为正整数是A除以B的商,所以:
>R=A−k∗B=aX−kbX=(a−kb)X>
因为a、k、b均为正整数,所以R也能被X整除
即A、B、R的公约数相同,所以有gcd(A,B) = gcd(B,A mod B)
这里我没有仔细的研究最大公约数和直线方程是否真的有什么联系,希望有大佬能给我解释一下。
解法3 求公约数避免除法(28ms左右)
这段代码没有优化,但是是一个基本的思路,引用的是其他人的办法
/*
* A line is determined by two factors,say y=ax+b
*
* If two points(x1,y1) (x2,y2) are on the same line(Of course).
* Consider the gap between two points.
* We have (y2-y1)=a(x2-x1),a=(y2-y1)/(x2-x1) a is a rational, b is canceled since b is a constant
* If a third point (x3,y3) are on the same line. So we must have y3=ax3+b
* Thus,(y3-y1)/(x3-x1)=(y2-y1)/(x2-x1)=a
* Since a is a rational, there exists y0 and x0, y0/x0=(y3-y1)/(x3-x1)=(y2-y1)/(x2-x1)=a
* So we can use y0&x0 to track a line;
*/
public int maxPoints(Point[] points) {
if (points == null) return 0;
if (points.length <= 2) return points.length;
Map> map = new HashMap>();
int result = 0;
for (int i = 0; i < points.length; i++) {
map.clear();
int overlap = 0, max = 0;
for (int j = i + 1; j < points.length; j++) {
int x = points[j].x - points[i].x;
int y = points[j].y - points[i].y;
if (x == 0 && y == 0) {
overlap++;
continue;
}
int gcd = generateGCD(x, y);
if (gcd != 0) {
x /= gcd;
y /= gcd;
}
if (map.containsKey(x)) {
if (map.get(x).containsKey(y)) {
map.get(x).put(y, map.get(x).get(y) + 1);
} else {
map.get(x).put(y, 1);
}
} else {
Map m = new HashMap();
m.put(y, 1);
map.put(x, m);
}
max = Math.max(max, map.get(x).get(y));
}
result = Math.max(result, max + overlap + 1);
}
return result;
}
private int generateGCD(int a, int b) {
if (b == 0) return a;
else return generateGCD(b, a % b);
}
解法3的魔改版(21ms)
我们有几个地方可以优化
- 求最大公约数可以用迭代的方式求,可以省去递归在栈上的开销。
- 我借用了一下HashMap的hash方法,所以我们只需要把这dx和dy进行hash就可以获得一个唯一的key,就省的用
Map
这么变态的数据结构来存储了,可以转用Map
来存储,反之添加一个hashCode(int,int)
方法,注意此处我写的不是很好,应该换个名字,因为所有的对象都有一个名为hashCode
的方法,虽然参数列表不同,但是如果在实际开发中遇到了类似的问题,一定要换一个名字。
有关hash算法的折跃门:
https://www.jianshu.com/p/bf1d7eee28d0
public int hashCode(int dx, int dy) {
//return Objects.hashCode(dx) ^ Objects.hashCode(dy);
return (dx << 32) ^ dy;
}
public int maxPoints(Point[] points) {
final int len = points.length;
if (len < 3) {
return len;
}
int max = 0;
for (int i = 0; i < len - 1; ++i) {
Map map = new HashMap<>(len);
int samePoint = 0;
int sameLine = 1;
for (int j = i + 1; j < len; ++j) {
if (points[i].x == points[j].x && points[i].y == points[j].y) {
++samePoint;
continue;
}
int dx = points[i].x - points[j].x;
int dy = points[i].y - points[j].y;
int gcd = gcd(dx, dy);
int commX = dx / gcd, commY = dy / gcd;
int hash = hashCode(commX, commY);
Integer val = map.get(hashCode(commX, commY));
if (val != null) {
map.put(hashCode(commX, commY), val + 1);
++val;
} else {
val = 2;zheyuemen
map.put(hashCode(commX, commY), val);
}
sameLine = Math.max(sameLine, val);
}
max = Math.max(max, samePoint + sameLine);
}
return max;
}
int gcd(int a, int b) {
int r = 0;
while (b != 0) {
r = a % b;
a = b;
b = r;
}
return a;
}
在LeetCode上的朋友@scn7th指出,之前的简单的求HashCode的方法可以通过AC,但是有碰撞的可能,是不合理的,
比如Testcase:
[1,1],[3,2],[5,3],[4,1],[2,3]
比如点[1,1]与[3,2],[5,3]在一条直线上,所得到用于表示斜率的hashCode是3,因为此时入参dx和dy分别为1,2;如果对应直线为[2,3]和[1,1],入参dx和dy为2,1,也会得到同样的结果3。但显然该点[2,3]不是在[1,1]与[3,2],[5,3]的直线上。
于是可以修改hashCode方法为
public int hashCode(int dx, int dy) {
//return Objects.hashCode(dx) ^ Objects.hashCode(dy);
return (dx << 32) ^ dy;
}
这样可以避免碰撞,对于两个32位整数而言,上面的做法都可以避免碰撞,Long是64位,那么int a左移32位然后异或上int b,非常巧妙的想法@scn7th
解法4 叉积法(19ms)
我只能说这个是O(n^3)
的解法,上面的是O(n*n*logn)
的解法,这个效率竟然会特么的更高,为啥呢,因为这个问题提供的测试案例不足,在跑完所有的testcase的时候解法3的渐进优势并没有体现出来,导致这个方法竟然是目前看来最优的。
叉积法,没有什么好解释的,就是数学公式,三层循环。
贴一个判断三点共线的传送门:
https://yiminghe.iteye.com/blog/568666
public int maxPoints(Point[] points) {
int res = 0, n = points.length;
for (int i = 0; i < n; ++i) {
int duplicate = 1;
for (int j = i + 1; j < n; ++j) {
int cnt = 0;
long x1 = points[i].x, y1 = points[i].y;
long x2 = points[j].x, y2 = points[j].y;
if (x1 == x2 && y1 == y2) {++duplicate;continue;}
for (int k = 0; k < n; ++k) {
int x3 = points[k].x, y3 = points[k].y;
if (x1*y2 + x2*y3 + x3*y1 - x3*y2 - x2*y1 - x1 * y3 == 0) {
++cnt;
}
}
res = Math.max(res, cnt);
}
res = Math.max(res, duplicate);
}
return res;
}
完全代码地址:https://github.com/anmingyu11/AlgorithmsUnion/blob/master/LeetCode/src/_java/_0149MaxPointsOnALine.java