Leetcode(149)——直线上最多的点数
题目
给你一个数组 pointspointspoints ,其中 points[i]=[xi,yi]points[i] = [xi, yi]points[i]=[xi,yi] 表示 X-Y 平面上的一个点。求最多有多少个点在同一条直线上。
示例 1:
输入:points = [[1,1],[2,2],[3,3]]
输出:3
示例 2:
输入:points = [[1,1],[3,2],[5,3],[4,1],[2,3],[1,4]]
输出:4
提示:
- 1 <= points.length <= 300
- points[i].length == 2
- -104 <= xi, yi <= 104
- points 中的所有点 互不相同
题解
关键是如何记录线——如何记录线的斜率保证不出误差(用乘法代替除法)还有一些要注意的看下文!
下面两个方法都是通过暴力枚举每一条线段,然后统计在同一条直线上最多的点。
数据结构(线)有三种方法————1.两点 2.斜率+截距 3.斜率+一点(用这个)
(用除法会有误差——用乘法避免除法的浮点误差)
我的错误:
我开始是想用一个数据结构存储每一条直线和其所占点数,而决定一条直线的是斜率和在X或Y轴上的截距。
所以使用外层 for 循环查找第 i 个点,内层 for 循环查找第 i 个点之后的点,以避免重复找到重复线段。一开始我想到了斜率相同不代表是同一条直线,这使我陷入两个误区:
- ①我们要不要保存直线在X或Y轴上的截距呢?
- ②用于存储每一条直线和其所占点个数的数据结构会一直保存全部数据直到出了两层 for 循环后再查找最大值来返回?
这就要求不能出现重复端点的情况。我们又知道在第一次找到某条直线时,在 这次的内层 for 循环 中会找到这条直线上的全部点(因为在一条直线上的任意两点连接形成的线段都在直线上),而在后面循环中还有可能再次遇到这条直线上的其它点。
比如 [[1,1], [2,2], [3,3]] ,在第一次内循环中,找到了 ([1,1], [2,2]) 和 ([1,1], [3,3]) 。而在在第二次内循环中,找到了 ([2,2], [3,3]) 。我们可以观察得到,在某条直线第一次被发现时,在这次内循环会找到该直线上的所有点,并且在后面的内循环中有可能会再次找到之前找到的一部分点。
但是我们可以发现,尽管在后面的内循环中有可能会再次找到之前找到的点,但那只是一部分,一定不会比之前找到的点更多,且一条直线能找到最多的点只会在第一次发现该直线的那一次的内循环中被找到。
所以,
- ①不需要保存直线在X或Y轴上的截距。因为如果以外层 for 循环的点为基点,那么过该点的直线在给定斜率下有且只有一条直线(即一点+斜率确定一条直线)。而斜率我们可以保存在数据结构中,至于点?在外层遍历的时就提供了。
- ②用于存储每一条直线和其所占点个数的数据结构,在内层 for 循环一次后,会将其中最多的点个数与存储最多点数的变量中的值相比较,若前者大于后者,则将前者的值赋予后者,否则什么都不干。最后清除这个数据结构的全部数据,重新开始一次内层 for 循环。
为什么不需要保存直线在X或Y轴上的截距还有一个重要的原因:
我们无需知道该直线是否和之前内循环中找到的相同斜率的直线是否是同一条。因为如果是同一条直线,那么这次内循环中找到的点一定没有上次的多(第一次找到的是全部,是最多的)。如果不是同一条,则这次找的点与上次没有任何关系。我们只需要找到有最多点的直线上点的个数,而不是找到那条直线。
方法一:枚举直线 + 枚举统计
思路
我们可以考虑枚举所有的点,假设直线经过该点时,该直线所能经过的最多的点数。
假设我们当前枚举到点 iii,如果直线同时经过另外两个不同的点 jjj 和 kkk,那么可以发现点 iii 和点 jjj 所连直线的斜率恰等于点 iii 和点 kkk 所连直线的斜率。
于是我们可以统计其他所有点与点 iii 所连直线的斜率,出现次数最多的斜率即为经过点数最多的直线的斜率,其经过的点数为该斜率出现的次数加一(点 iii 自身也要被统计)。
如何记录斜率:
需要注意的是,浮点数类型可能因为精度不够而无法足够精确地表示每一个斜率,因此我们需要换一种方法来记录斜率。
一般情况下,斜率可以表示为 slope=ΔyΔx\textit{slope} = \dfrac{\Delta y}{\Delta x}slope=ΔxΔy 的形式,因此我们可以用分子和分母组成的二元组来代表斜率。但注意到存在形如 12=24\dfrac{1}{2}=\dfrac{2}{4}21=42 这样两个二元组不同,但实际上两分数的值相同的情况,所以我们需要将分数 ΔyΔx\dfrac{\Delta y}{\Delta x}ΔxΔy 化简为最简分数的形式。
将分子和分母同时除以二者绝对值的最大公约数,可得二元组 (Δxgcd(∣Δx∣,∣Δy∣),Δygcd(∣Δx∣,∣Δy∣))\Big(\dfrac{\Delta x}{\gcd(|\Delta x|,|\Delta y|)},\dfrac{\Delta y}{\gcd(|\Delta x|,|\Delta y|)}\Big)(gcd(∣Δx∣,∣Δy∣)Δx,gcd(∣Δx∣,∣Δy∣)Δy)。令 mx=Δxgcd(∣Δx∣,∣Δy∣)\textit{mx}=\dfrac{\Delta x}{\gcd(|\Delta x|,|\Delta y|)}mx=gcd(∣Δx∣,∣Δy∣)Δx,my=Δygcd(∣Δx∣,∣Δy∣)\textit{my}=\dfrac{\Delta y}{\gcd(|\Delta x|,|\Delta y|)}my=gcd(∣Δx∣,∣Δy∣)Δy,则上述化简后的二元组为 (mx,my)(\textit{mx},\textit{my})(mx,my)。
此外,因为分子分母可能存在负数,为了防止出现形如 −12!=1−2\dfrac{-1}{2} != \dfrac{1}{-2}2−1!=−21 的情况,我们还需要规定分子为非负整数,如果 my\textit{my}my 为负数,我们将二元组中两个数同时取相反数即可。
特别地,考虑到 mx\textit{mx}mx 和 my\textit{my}my 两数其中有一个为 000 的情况(因为题目中不存在重复的点,因此不存在两数均为 000 的情况),此时两数不存在数学意义上的最大公约数,因此我们直接特判这两种情况。当 mx\textit{mx}mx 为 000 时,我们令 my=1\textit{my}=1my=1;当 my\textit{my}my 为 000 时,我们令 mx=1\textit{mx}=1mx=1 即可。
经过上述操作之后,即可得到最终的二元组 (mx,my)(\textit{mx},\textit{my})(mx,my)。在本题中,因为点的横纵坐标取值范围均为 [−104,104][-10^4, 10^4][−104,104],所以斜率 slope=mymx\textit{slope} = \dfrac{\textit{my}}{\textit{mx}}slope=mxmy中,mx\textit{mx}mx 落在区间 [−2×104,2×104][- 2 \times 10^4, 2 \times 10^4][−2×104,2×104] 内,my\textit{my}my 落在区间 [0,2×104][0, 2 \times 10^4][0,2×104] 内(因为前面我们规定了分子为非负整数)。
我们还注意到 323232 位整数的范围远超这两个区间,因此我们可以用单个 323232 位整型变量来表示这两个整数。具体地,我们令 val=my+(2×104+1)×mx\textit{val} = \textit{my} + (2 \times 10^4 + 1) \times \textit{mx}val=my+(2×104+1)×mx 即可。这样可以把两个键值组合成一个键值。在哈希表中查询 val 比查询 (my,mx) 要方便的多。然后 val 的最大值是 2×104+(2×104+1)×2×104<232−12\times10^{4} + (2\times10^{4}+1)\times 2\times 10^{4} < 2^{32-1}2×104+(2×104+1)×2×104<232−1 的,所以可以用32位的 int 型储存。至于为什么要用这个公式,可以把上面两个区间组合成的图形看成 2×104+12\times10^4+12×104+1 列的矩阵吧,要把二维矩阵映射到一维矩阵的公式为 i = x * 列数 + y (或 i = x + y * 行数)。
优化:
- 在点的总数量小于等于 222 的情况下,我们总可以用一条直线将所有点串联,此时我们直接返回点的总数量即可;
- 当我们枚举到点 iii 时,我们只需要考虑编号大于 iii 的点到点 iii 的斜率,因为如果直线同时经过编号小于点 iii 的点 jjj,那么当我们枚举到 jjj 时就已经考虑过该直线了;
- 当我们找到一条直线经过了图中超过半数的点时,我们即可以确定该直线即为经过最多点的直线;
- 当我们枚举到点 iii(假设编号从 000 开始)时,我们至多只能找到 n−in-in−i 个点共线。假设此前找到的共线的点的数量的最大值为 kkk,如果有 k≥n−ik \geq n-ik≥n−i,那么此时我们即可停止枚举,因为不可能再找到更大的答案了。
代码实现
class Solution {
public:
int maxPoints(vector<vector<int>>& points) {
int len = points.size();
// 点的数量不够
if(len < 3) {
return len;
}
int maxNum = 2;
// 遍历每两个点
for(int i = 0; i < len - 1; i ++) {
for(int j = i + 1; j < len; j ++) {
// 统计斜率相等个数
int count = 2;
long long dx = points[i][0] - points[j][0];
long long dy = points[i][1] - points[j][1];
// 与其他点比较
for(int k = j + 1; k < len; k ++) {
// 如果斜率相等
if(dx * (points[i][1] - points[k][1]) == dy * (points[i][0] - points[k][0])) {
count ++;
}
}
maxNum = max(maxNum, count);
if(maxNum > len / 2) return maxNum;
}
}
return maxNum;
}
};
复杂度分析
时间复杂度:O(n3)O(n^3)O(n3) ,其中 nnn 是数组 pointspointspoints 的元素个数,即点的个数。
空间复杂度:O(1)O(1)O(1)
方法二:枚举直线 + 哈希表统计
思路
整体思路和方法一类似,只是这里我们使用哈希表将每次对直线的查找优化为 O(1)O(1)O(1)。
代码实现
class Solution {
private:
int gcd(int a, int b) { // 求最大公约数
return b ? gcd(b, a % b) : a;
}
public:
int maxPoints(vector<vector<int>>& points) {
unordered_map<int,int> line;
int max=1,xc,yc,maxgcd,k;
if(points.size() <= 2)
return points.size();
for(auto i=points.begin(); i!=points.end()-1; i++){
for(auto t=i+1; t!=points.end(); t++){
xc = (*i)[0]-(*t)[0];
yc = (*i)[1]-(*t)[1];
if(yc != 0){
if(yc < 0){
xc = -xc;
yc = -yc;
}
maxgcd = gcd(abs(xc),abs(yc)); // abs 是STL函数,用于取绝对值
if(maxgcd != 0 && maxgcd != 1){
xc = xc / maxgcd;
yc = yc / maxgcd;
}
k = (2*10^4+1)*xc+yc;
if(line.count(k) == 0){
line.emplace(k, 2);
}else line.find(k)->second += 1;
}else{
k = sizeof(int); // 给斜率取一个永远不会被取到的数字
if(line.count(k) == 0){
line.emplace(k, 2);
}else line.find(k)->second += 1;
}
}
for(auto n:line){
max = std::max(max, n.second);
}
line.clear();
}
return max;
}
};
复杂度分析
时间复杂度:O(n2×logm)O(n^2 \times \log m)O(n2×logm) ,其中 nnn 是数组 pointspointspoints 的元素个数,即点的个数。其中 mmm 为横纵坐标差的最大值。最坏情况下我们需要枚举所有 nnn 个点,枚举单个点过程中需要进行 O(n)O(n)O(n) 次最大公约数计算,单次最大公约数计算的时间复杂度是 O(logm)O(\log m)O(logm)——因为是递归算法,因此总时间复杂度为 O(n2×logm)O(n^2 \times \log m)O(n2×logm)。
空间复杂度:O(n)O(n)O(n) ,其中 nnn 为点的个数,主要是哈希表的开销。