Find the maximum number of points lying on the same straight line using slope counting and normalization.
Problem Restatement
We are given a list of 2D points:
points[i] = [xi, yi]Return the maximum number of points that lie on the same straight line.
A line may be horizontal, vertical, diagonal, or any slope.
LeetCode constrains the number of points to at most 300, which allows an O(n^2) solution. (leetcode.com)
Examples
Example 1:
points = [[1,1],[2,2],[3,3]]All three points lie on:
y = xOutput:
3Example 2:
points = [[1,1],[3,2],[5,3],[4,1],[2,3],[1,4]]One maximum line contains:
(1,1), (3,2), (5,3)Output:
3Input and Output
| Item | Meaning |
|---|---|
| Input | points: list[list[int]] |
| Output | Maximum number of collinear points |
| Constraint | 1 <= len(points) <= 300 |
| Coordinates | Integers |
Function shape:
class Solution:
def maxPoints(self, points: list[list[int]]) -> int:
...Key Observation
Two points determine a unique line.
So for each point:
points[i]we can treat it as an anchor and count how many other points form the same slope relative to it.
If many points share the same slope from the anchor point, then they all lie on the same line.
Basic Idea
Fix one point:
(x1, y1)For every other point:
(x2, y2)compute:
dy = y2 - y1
dx = x2 - x1The slope is:
dy / dxCount how many times each slope appears.
The largest slope frequency plus the anchor point gives the largest line through that anchor.
Take the global maximum across all anchors.
Why Floating Point Is Dangerous
Suppose:
dy = 1
dx = 3The slope is:
0.333333...Floating point precision can introduce rounding issues.
Instead, represent slopes as reduced integer pairs:
(dy, dx)normalized by greatest common divisor.
Example:
(2, 4)becomes:
(1, 2)Now equal slopes have identical representations.
Normalizing Slopes
Suppose:
dy = 6
dx = 9Compute:
gcd(6, 9) = 3Normalize:
(6 // 3, 9 // 3)
= (2, 3)Now every equivalent slope maps to the same pair.
Handling Signs
These are the same slope:
(1, 2)
(-1, -2)We must normalize signs consistently.
A common rule:
dx > 0If dx < 0, multiply both by -1.
For vertical lines:
dx = 0normalize to:
(1, 0)For horizontal lines:
dy = 0normalize to:
(0, 1)Example Walkthrough
Consider:
points = [[1,1],[2,2],[3,3],[4,5]]Anchor:
(1,1)Other points:
| Point | (dy, dx) | Reduced |
|---|---|---|
(2,2) | (1,1) | (1,1) |
(3,3) | (2,2) | (1,1) |
(4,5) | (4,3) | (4,3) |
Slope counts:
| Slope | Count |
|---|---|
(1,1) | 2 |
(4,3) | 1 |
Largest count:
2Add the anchor point:
2 + 1 = 3So the maximum line through (1,1) contains 3 points.
Algorithm
For each point i:
- Create an empty slope counter.
- Compare point
iwith every later pointj. - Compute normalized slope
(dy, dx). - Count how many times each slope occurs.
- Track the largest slope frequency for this anchor.
- Update the global maximum.
Correctness
Fix an anchor point p.
Any other point q forms exactly one line through p, determined by the slope between p and q.
All points collinear with p share the same slope relative to p. After normalization, all equivalent slopes map to the same (dy, dx) pair.
Therefore, counting equal normalized slopes counts exactly the number of points on the same line through p.
For each anchor point, the algorithm computes the maximum number of collinear points through that anchor.
Since every line contains at least one anchor point considered during the outer loop, the global maximum across all anchors equals the maximum number of collinear points in the entire set.
Thus, the algorithm returns the correct answer.
Complexity
Let:
| Symbol | Meaning |
|---|---|
n | Number of points |
For each point, we compare against all later points.
| Metric | Value | Why |
|---|---|---|
| Time | O(n^2) | Pairwise comparisons |
| Space | O(n) | Slope hash map |
This is efficient enough for:
n <= 300Implementation
from collections import defaultdict
from math import gcd
class Solution:
def maxPoints(self, points: list[list[int]]) -> int:
n = len(points)
if n <= 2:
return n
answer = 1
for i in range(n):
slopes = defaultdict(int)
x1, y1 = points[i]
local_max = 0
for j in range(i + 1, n):
x2, y2 = points[j]
dy = y2 - y1
dx = x2 - x1
if dx == 0:
dy = 1
dx = 0
elif dy == 0:
dy = 0
dx = 1
else:
if dx < 0:
dy = -dy
dx = -dx
g = gcd(abs(dy), abs(dx))
dy //= g
dx //= g
slopes[(dy, dx)] += 1
local_max = max(
local_max,
slopes[(dy, dx)],
)
answer = max(answer, local_max + 1)
return answerCode Explanation
If there are at most two points:
if n <= 2:
return nall points trivially lie on one line.
For each anchor point:
for i in range(n):create a slope counter:
slopes = defaultdict(int)Compute slope differences:
dy = y2 - y1
dx = x2 - x1Handle vertical lines:
if dx == 0:
dy = 1
dx = 0Handle horizontal lines:
elif dy == 0:
dy = 0
dx = 1Otherwise normalize:
g = gcd(abs(dy), abs(dx))Then reduce:
dy //= g
dx //= gCount occurrences:
slopes[(dy, dx)] += 1The line count through this anchor is:
local_max + 1because the counter only counts the other points, not the anchor itself.
Why We Only Compare j > i
This avoids duplicate work.
The pair:
(i, j)represents the same geometric relation as:
(j, i)So we only process each pair once.
Testing
def run_tests():
s = Solution()
assert s.maxPoints([
[1, 1],
[2, 2],
[3, 3],
]) == 3
assert s.maxPoints([
[1, 1],
[3, 2],
[5, 3],
[4, 1],
[2, 3],
[1, 4],
]) == 3
assert s.maxPoints([
[1, 1],
]) == 1
assert s.maxPoints([
[1, 1],
[1, 2],
[1, 3],
]) == 3
assert s.maxPoints([
[1, 1],
[2, 1],
[3, 1],
[4, 1],
]) == 4
assert s.maxPoints([
[0, 0],
[1, 1],
[-1, -1],
]) == 3
print("all tests passed")
run_tests()Test meaning:
| Test | Why |
|---|---|
| Diagonal line | Standard slope case |
| Mixed points | Multiple candidate lines |
| Single point | Smallest input |
| Vertical line | dx = 0 normalization |
| Horizontal line | dy = 0 normalization |
| Negative coordinates | Sign normalization correctness |