[LeetCode]149 MaxPointsOnALine (最完全攻略)

原创,转载注明出处,最后修正日期 2019.1.26


能力一般,水平有限.还希望各位大牛们批评指正.


题目传送门:https://leetcode.com/problems/max-points-on-a-line/

这个是全leetcode上,通过率第二低的题,也是第二难的题。
在LeetCode上统计中,Alibaba的面试中和Baidu的面试中也出现过这个问题,不过你要是碰到这道题也算是面试官看得起你了,中奖了兄台。

回归正题:其实这道题没tm啥技术含量,就是巨麻烦。
在网上没有看到很好很完善的中文教程,于是打算自己写一个。

1935121-e7f64687a20b7c0e.png
难度排名

那么我们现在一步一步的来干掉她。


描述:

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<Double, Integer> 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<Integer, Map<Integer, Integer>> map = new HashMap<Integer, Map<Integer, Integer>>();
            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<Integer, Integer> m = new HashMap<Integer, Integer>();
                        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)

我们有几个地方可以优化

  1. 求最大公约数可以用迭代的方式求,可以省去递归在栈上的开销。
  2. 我借用了一下HashMap的hash方法,所以我们只需要把这dx和dy进行hash就可以获得一个唯一的key,就省的用Map<Map<Integer,Integer>,Integer>这么变态的数据结构来存储了,可以转用Map<Integer,Integer>来存储,反之添加一个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);
        }

        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<Integer, Integer> 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;
        }

解法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

猜你喜欢

转载自blog.csdn.net/weixin_33862041/article/details/87039239