149. Maximum Points on a Line - Explanation
Prerequisites
Before attempting this problem, you should be comfortable with:
- Hash Maps - Using dictionaries to group and count elements by key in O(1) average time
- Coordinate Geometry - Understanding slopes and collinearity of points on a 2D plane
- Greatest Common Divisor (GCD) - Reducing fractions to canonical form to avoid floating point precision issues
- Nested Loops - Iterating through pairs or triplets of elements for comparison
1. Math
Intuition
Points are collinear if they share the same slope when measured from a common reference point. For each pair of points, we compute the slope and then check how many other points lie on that same line. By fixing two points and iterating through all remaining points to count matches, we can find the maximum number of collinear points. This triple nested loop is simple but has cubic time complexity.
Algorithm
- If there are 2 or fewer points, return the count directly since all such points are trivially collinear.
- For each pair of points
(i, j), compute their slope. - For every other point
k, check if the slope fromitokmatches. If so, increment the count. - Track the maximum count across all pairs.
- Return the maximum.
class Solution:
def maxPoints(self, points: List[List[int]]) -> int:
n = len(points)
if n <= 2:
return n
def get_slope(p1, p2):
if p1[0] == p2[0]:
return float("inf")
return (p2[1] - p1[1]) / (p2[0] - p1[0])
res = 1
for i in range(n):
for j in range(i + 1, n):
slope = get_slope(points[i], points[j])
cnt = 2
for k in range(j + 1, n):
if slope == get_slope(points[i], points[k]):
cnt += 1
res = max(res, cnt)
return resTime & Space Complexity
- Time complexity:
- Space complexity: extra space.
2. Math + Hash Map
Intuition
Instead of checking every pair and then scanning all other points, we can fix one point and use a hash map to group all other points by their slope relative to the fixed point. Points with the same slope lie on the same line through the fixed point. The largest group size plus one (for the fixed point itself) gives the maximum collinear points for that reference. Repeating this for every point as the reference yields the global maximum.
Algorithm
- Initialize
resto1(at least one point exists). - For each point
i, create a hash map to count slopes. - For every other point
j, compute the slope fromitojand increment its count in the map. - Update
reswithcount + 1(including pointi). - Return
resafter processing all points.
class Solution:
def maxPoints(self, points: List[List[int]]) -> int:
res = 1
for i in range(len(points)):
p1 = points[i]
count = defaultdict(int)
for j in range(i + 1, len(points)):
p2 = points[j]
if p2[0] == p1[0]:
slope = float("inf")
else:
slope = (p2[1] - p1[1]) / (p2[0] - p1[0])
count[slope] += 1
res = max(res, count[slope] + 1)
return resTime & Space Complexity
- Time complexity:
- Space complexity:
3. Math + Hash Map (Optimal)
Intuition
Floating point slopes can introduce precision errors. To avoid this, we represent the slope as a reduced fraction using the GCD of the differences in x and y coordinates. By storing slopes as pairs (or strings) of integers rather than floating point numbers, we eliminate rounding issues. This approach maintains the same algorithmic structure as the previous solution but with more robust comparisons.
Algorithm
- If there are
2or fewer points, return the count directly. - For each point
i, create a hash map keyed by the normalized slope (using GCD reduction). - For every other point
j:- Compute
dx = x_j - x_ianddy = y_j - y_i. - Divide both by their GCD to get the canonical slope representation.
- Increment the count for this slope in the map.
- Compute
- Update
reswith the maximum count plus1. - Return
res.
class Solution:
def maxPoints(self, points: List[List[int]]) -> int:
if len(points) <= 2:
return len(points)
def gcd(a, b):
return gcd(b, a % b) if b else a
res = 1
for i in range(len(points) - 1):
count = defaultdict(int)
for j in range(i + 1, len(points)):
dx = points[j][0] - points[i][0]
dy = points[j][1] - points[i][1]
g = gcd(dx, dy)
dx //= g
dy //= g
slope = (dx, dy)
count[slope] += 1
res = max(res, max(count.values()) + 1)
return resTime & Space Complexity
- Time complexity:
- Space complexity:
Where is the number of points and is the maximum value in the points.
Common Pitfalls
Floating Point Precision Errors
Using floating point division to compute slopes introduces precision errors. Two mathematically equal slopes may compare as unequal due to rounding. For example, 1/3 and 2/6 might not be exactly equal as floats. The solution is to represent slopes as reduced fractions using GCD, storing them as pairs or strings of integers.
Handling Vertical Lines
When two points have the same x-coordinate, the slope is undefined (division by zero). Forgetting to handle this case causes runtime errors. Use a special value like infinity or a distinct key (such as "inf" or a tuple like (1, 0)) to represent vertical lines in your slope map.
Negative Zero in Floating Point
In floating point arithmetic, -0.0 and 0.0 are distinct values but should be treated as equal slopes. Horizontal lines going left-to-right and right-to-left produce 0.0 and -0.0 respectively. When using floating point slopes, normalize -0.0 to 0.0 before using it as a hash key.
Inconsistent Slope Normalization with GCD
When using GCD to reduce fractions, ensure consistent sign handling. The GCD of negative numbers can produce inconsistent signs across different implementations. Normalize by always making the denominator positive, or by making the first non-zero component positive. Otherwise, slopes like (-1, 2) and (1, -2) will be treated as different when they represent the same line.
Forgetting to Include the Reference Point
When counting points with the same slope from a reference point, the count only includes other points, not the reference point itself. The total collinear points is count + 1. Forgetting to add one for the reference point will undercount by one for every line.