LeetCode #149 Max Points on a Line
(Week 3 演算法作業)
題目
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
Difficulty: Hard
分析
要求出在同一條直線上最多的點數。
有一些需要注意的特殊情況:
1. 輸入中可能有多個座標相同的點,它們應當被看作不同的點。
如:
Input: [[0,0],[1,1],[0,0]]
Expected Output: 3
Input: [[1,1],[1,1],[1,1]]
Expected Output: 3
2. 輸入中可能有很接近的兩條直線。
Input: [[0,0],[94911151,94911150],[94911152,94911151]]
Expected Output: 2
如果直接用 double
除法去算這三個點兩兩相連產生的直線的斜率和截距,就會得出錯誤的輸出:3。
所以,需要使用分數來表示直線的斜率和截距(如果你需要記錄它們的話)。
12ms演算法
演算法思路:
-
使用自定義的結構體
Line
來表示直線,使用map<Line, int>
來記錄每條直線上點對的個數。 -
對每個點
points[i]
,從j=i+1
開始向後掃描points[j]
,設輸入有 n 個點,第1個點要往後掃描 (n-1) 個點,第2個點要往後掃描 (n-2) 個點,以此類推,共掃描 個點對。 -
每掃描一個點對,就檢測這個點對的連線。在
map
裡將這條線對應的值增加1,即有新的一個點對在這條線上。 -
如果某個點對中的兩個點相同,所有經過這個點的直線在
map
-
最後,找出map中對映值最大的一個鍵,即穿過點數最多的線。假設這條線穿過 k 個點,鍵的對映值為 m ,那麼這條直線上會有 個點對,有 ,可算得 。
/**
* Definition for a point.
* struct Point {
* int x;
* int y;
* Point() : x(0), y(0) {}
* Point(int a, int b) : x(a), y(b) {}
* };
*/
typedef pair<int,int> paint;
// 求最大公約數
int measure(int x, int y)
{
int z = y;
while(x%y!=0)
{
z = x%y;
x = y;
y = z;
}
return z;
}
struct Line {
paint a; // 斜率(當直線為豎線時,a=(x,0))
paint b; // 截距(當直線為豎線時,b=(1,0))
Line() : a(paint(0, 0)), b(paint(0, 0)){}
Line(int m, int n, int x, int y) : a(paint(m, n)), b(paint(x, y)) {}
bool operator < (const Line &l2) const{
return a < l2.a || a == l2.a && b < l2.b;
}
double get(int x) const{ // 獲取直線上與橫座標x對應的縱座標
int gcd = measure(a.second, b.second);
int aa = b.second / gcd;
int bb = a.second / gcd;
int m = a.first * x * aa + b.first * bb;
int n = a.second * aa;
return (double)m / (double)n;
}
};
class Solution {
public:
int maxPoints(vector<Point>& points) {
if(points.size() < 3) return points.size();
map<Line, int> lines; // 鍵為直線,對映值為在這條直線上的點對的個數
for(int i = 0; i < points.size() - 1; i++){
int same = 0; // 記錄掃描points[j]時遇到的與points[i]相同的點
set<int*> s; // 記錄points[i]與points[j]的連線在map中的對映值的指標
for(int j = i + 1; j < points.size(); j++){
int deltax = points[i].x - points[j].x;
int deltay = points[i].y - points[j].y;
if(deltax == 0 && deltay == 0){ // 兩個點相同
same++;
}
else if(deltax == 0 && deltay != 0){ // 兩個點在同一豎線上
int x = points[i].x;
int& num = lines[Line(x, 0, 1, 0)];
num += 1;
s.insert(&num);
}
else{
int gcd = measure(deltay, deltax);
int a1 = deltay / gcd;
int a2 = deltax / gcd;
int m = points[i].y * deltax - deltay * points[i].x;
int gcd2 = measure(m, deltax);
int b1 = m / gcd2;
int b2 = deltax / gcd2;
int& num = lines[Line(a1, a2, b1, b2)];
num += 1;
s.insert(&num);
}
}
// 有same個與points[i]相同的點,相當於還有same個點對在經過這個點的直線上
// 需要將直線的對映值增加相應的數量
for(int* num : s){
*num += same;
}
// 如果集合s為空,說明掃描到的points[j]全都與points[i]相同
// 需要掃描lines,看是否有經過該點的直線
if(s.empty()){
for(auto& l : lines){
if(l.first.get(points[i].x) == points[i].y){
l.second += same;
}
}
}
}
// 輸入的所有點全都相同的情況
if(lines.size() == 0) return points.size();
// 找出最大的點對數
int max = 1;
map<Line, int>::iterator iter = lines.begin();
while(iter != lines.end()){
if(iter->second > max) max = iter->second;
iter++;
}
// 由點對數量算出點數
max = (sqrt(8 * max + 1) + 1) / 2;
return max;
}
};
演算法複雜度為。
其他優秀演算法
8ms演算法
sample①
演算法思路:
-
對每個點對,遍歷points,看有幾個點和這個點對在同一直線上。
-
遍歷每個點對,進行上述操作,找出最大的點數。
/**
* Definition for a point.
* struct Point {
* int x;
* int y;
* Point() : x(0), y(0) {}
* Point(int a, int b) : x(a), y(b) {}
* };
*/
class Solution {
public:
int maxPoints(vector<Point>& points) {
int res = 0 ;
for( int i = 0 ; i < points.size() ; i ++){ //first point.
int duplicate = 1 ;
for( int j = i + 1 ; j < points.size() ; j++){ //second point.
int count = 0 ;
long long x1 = points[i].x , y1 = points[i].y ; //use long type to avoid overflow.
long long x2 = points[j].x , y2 = points[j].y ;
if( x1 == x2 && y1 == y2 ){ //if two points are duplicated.
duplicate ++ ;
continue ;
}
for( int k = 0 ; k < points.size() ; k++){ //find the third point.
int x3 = points[k].x , y3 = points[k].y ;
if( x1 * y2 + x2 * y3 + x3 * y1 - x3 * y2 - x2 * y1 - x1 * y3 == 0 ) //uses determinant multiplication.
count ++ ;
}
res = max(res , count) ;
}
res = max(res , duplicate) ;
}
return res ;
}
};
雖然演算法複雜度為,但是操作比較簡單,不用排除重複的直線;且沒有使用有複雜操作的STL,用時反而較短。思路也很簡捷。
sample②
演算法思路:
-
對每個
points[i]
,建立一個unordered_map<int, unordered_map<int, int>>
型別的line
,line[x][y]
記錄了與points[i]
有斜率為y/x
的連線的points[j]
的個數。因為過一個點的兩條不同直線必有不同的斜率,所以可以確定這些points[j]
都在同一條直線上。 -
因為max points事實上是確定的,所以對每個
points[i]
,可以不從j = 0
開始掃描points[j]
。因為只要掃描points[i]
掃描到了max points對應的直線的第一個點,那麼這輪迴圈所確定的maxcnt
必然是最終答案,所以對前面或後面的points[i]
,是否從j = 0
開始掃描points[j]
都是沒關係的。 -
每次迴圈時都把計算出的點數與上一輪迴圈確定的
maxcnt
或count
比較,層層迴圈最終確定maxcnt
。
class Solution {
int gcd (int x, int y) {
if (x > y)
std::swap(x, y);
while (x > 0) {
int tmp = x;
x = y % x;
y = tmp;
}
return y;
}
public:
int maxPoints(vector<Point>& points) {
int n = points.size(), maxcnt = 0;
if (n <= 2) return n;
for (int i = 0; i < n; i++) {
int overlap = 0, count = 0;
unordered_map<int, unordered_map<int, int>> line;
for (int j = i + 1; j < n; j++) {
int dx = points[j].x - points[i].x;
int dy = points[j].y - points[i].y;
if (dx == 0 && dy == 0)
overlap++;
else {
if (dx < 0)
dx = -dx, dy = -dy;
else if (dx == 0)
dy = std::abs(dy);
int dvs = gcd(dx, std::abs(dy));
if (dvs != 0)
dx /= dvs, dy /= dvs;
count = std::max(count, ++line[dx][dy]);
}
}
maxcnt = std::max(maxcnt, count + overlap + 1);
}
return maxcnt;
}
};
演算法複雜度為。
4ms演算法
sample①
演算法思路:
類似於上面的8ms演算法的sample②。
但是因為沒有采用巢狀的 unordered_map
結構,而是使用 pair<int, int>
來表示斜率,所以比前一個演算法更快。
class Solution {
public:
int maxPoints(vector<Point>& points) {
int result = 0;
for (size_t i = 0; i < points.size(); ++i) {
Point pt1 = points[i];
std::unordered_map<std::pair<int, int>, int, pair_hash> slope_map;
int same_count = 0, v_count = 0, h_count = 0;
for (size_t j = i + 1; j < points.size(); ++j) {
Point pt2 = points[j];
if (pt2.x == pt1.x && pt2.y == pt1.y) {
++same_count;
} else if (pt2.x == pt1.x) {
++v_count;
} else if (pt2.y == pt1.y) {
++h_count;
} else {
int dx = pt2.x - pt1.x;
int dy = pt2.y - pt1.y;
int gcd = __gcd(dx, dy);
std::pair<int, int> key(dx / gcd, dy / gcd);
++slope_map[key];
}
}
int max_slope_count = 0;
for (auto it = slope_map.begin(); it != slope_map.end(); ++it) {
max_slope_count = std::max(max_slope_count, it->second);
}
max_slope_count = std::max(max_slope_count, v_count);
max_slope_count = std::max(max_slope_count, h_count);
result = std::max(result, max_slope_count + 1 + same_count);
}
return result;
}
private:
struct pair_hash {
template<typename U, typename V>
size_t operator() (const std::pair<U, V> & key_) const {
size_t result = std::hash<U>()(key_.first);
result += result * 31 + std::hash<V>()(key_.second);
return result;
}
};
};
演算法複雜度為 。
sample②
演算法思路:
在總體思路上,也類似於上面的8ms演算法的sample②和4ms演算法的sample①,只是細節處有所不同。
// 演算法是,遍歷每個點,每個點都有一個map,以該點為基點往下找點,組成一條直線,並在map裡找到這條直線,並將它的數目加1.
// 需要注意的是,第一,最後map裡的點,是不包括該點的,此外,往下找點時,如果遇到跟該點一樣的點,也需要做另外處理。最後的數目,需要加上該點以及它的duplicate的數目
// 第二,斜率如果用double儲存,精度會不夠
// 這裡斜率多寫了一個結構體,會約掉最大公約數,這樣的話,只有y和x都相等的情況下,才相等。然後再用 斜率a = ay * bx和斜率 b = by * ax來進行比較,這裡就算相等,也當是小於來看待!!!,comparator只需要在小於的情況下返回true,大於等於是false!!
/**
* Definition for a point.
* struct Point {
* int x;
* int y;
* Point() : x(0), y(0) {}
* Point(int a, int b) : x(a), y(b) {}
* };
*/
class Solution {
public:
static int gcd (int a, int b)
{
if (b == 0)
return a;
else
return gcd(b, a % b);
}
struct Slope
{
Slope() : x(0), y(0) {};
Slope(int ix, int iy)
{
int g = gcd(ix, iy);
y = iy / g;
x = ix / g;
};
int y;
int x;
};
struct MyCmp
{
bool operator()(const Slope &a, const Slope &b) const
{
if (a.x == b.x && a.y == b.y)
{
return false;
}
long long da = a.y * b.x;
long long db = b.y * a.x;
return da <= db;
}
};
int maxPoints(vector<Point>& points) {
if (points.size() < 3)
return points.size();
int res = 0;
for (int i = 0; i < points.size(); i++)
{
map<Slope, int, MyCmp> lines;
int duplicated = 1; // 1 means point i itself. if We encounter point i's duplicates later, plus one;
for(int j = i + 1; j < points.size(); j++)
{
Slope slope;
if (points[i].x == points[j].x && points[i].y == points[j].y)
{
duplicated++; //產生duplicate,不需要再進行斜率的考慮了
continue;
}
slope = Slope(points[i].y - points[j].y, points[i].x - points[j].x);
lines[slope]++;
}
res = max(res, duplicated);
for (auto it : lines)
{
res = max(res, it.second + duplicated);
}
}
return res;
}
};
結語
比較一下12ms和8ms②、4ms①②的演算法,可以看出,前者在迴圈之外使用 map
來記錄所有的直線,在迴圈結束後再尋找max points,就需要考慮截距的問題,而且佔用空間也比較多;後者在 points[i]
迴圈內使用 map
或 unordered_map
,每迴圈一次更新一次max points,就沒有前者的這些問題。
這次做的並不算好,僅僅beats不到一半的提交。
但通過研讀多個sample演算法、比較思路,從別人的不同的角度去看問題,我也很有收穫。