1. 程式人生 > >LeetCode #149 Max Points on a Line

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演算法

演算法思路:

  1. 使用自定義的結構體 Line來表示直線,使用 map<Line, int>來記錄每條直線上點對的個數。

  2. 對每個點 points[i],從 j=i+1開始向後掃描 points[j],設輸入有 n 個點,第1個點要往後掃描 (n-1) 個點,第2個點要往後掃描 (n-2) 個點,以此類推,共掃描 n(n1)/2n(n-1)/2 個點對。

  3. 每掃描一個點對,就檢測這個點對的連線。在 map裡將這條線對應的值增加1,即有新的一個點對在這條線上。

  4. 如果某個點對中的兩個點相同,所有經過這個點的直線在 map

    中的值都要增加1。

  5. 最後,找出map中對映值最大的一個鍵,即穿過點數最多的線。假設這條線穿過 k 個點,鍵的對映值為 m ,那麼這條直線上會有 k(k1)/2k(k-1)/2 個點對,有 k(k1)/2=mk(k-1)/2=m ,可算得 k=(8m+1+1)/2k=(\sqrt{8m+1}+1)/2

/**
 * 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;
		
    }
};

演算法複雜度為O(n2)O(n^2)

其他優秀演算法

8ms演算法

sample①

演算法思路:

  1. 對每個點對,遍歷points,看有幾個點和這個點對在同一直線上。

  2. 遍歷每個點對,進行上述操作,找出最大的點數。

/**
 * 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 ;   
    }
};

雖然演算法複雜度為O(n3)O(n^3),但是操作比較簡單,不用排除重複的直線;且沒有使用有複雜操作的STL,用時反而較短。思路也很簡捷。

sample②

演算法思路:

  1. 對每個 points[i] ,建立一個 unordered_map<int, unordered_map<int, int>> 型別的 lineline[x][y] 記錄了與 points[i] 有斜率為 y/x 的連線的 points[j] 的個數。因為過一個點的兩條不同直線必有不同的斜率,所以可以確定這些 points[j] 都在同一條直線上。

  2. 因為max points事實上是確定的,所以對每個 points[i] ,可以不從 j = 0 開始掃描 points[j] 。因為只要掃描 points[i] 掃描到了max points對應的直線的第一個點,那麼這輪迴圈所確定的 maxcnt 必然是最終答案,所以對前面或後面的points[i],是否從 j = 0 開始掃描 points[j] 都是沒關係的。

  3. 每次迴圈時都把計算出的點數與上一輪迴圈確定的 maxcntcount 比較,層層迴圈最終確定 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;
    }
};

演算法複雜度為O(n2)O(n^2)

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

演算法複雜度為 O(n2)O(n^2)

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] 迴圈內使用 mapunordered_map ,每迴圈一次更新一次max points,就沒有前者的這些問題。

這次做的並不算好,僅僅beats不到一半的提交。

但通過研讀多個sample演算法、比較思路,從別人的不同的角度去看問題,我也很有收穫。