TL;DR
For each anchor point, hash the reduced-fraction slope to every other point and take the biggest bucket β O(nΒ²) time, O(n) space.
Approach 1 β Brute force: test every triple
For every pair (i, j), count how many points lie on the line through them by testing all other points for collinearity with the cross-product test.
Collinearity of A, B, C holds when the signed area of the triangle is zero: (B.x - A.x) * (C.y - A.y) - (B.y - A.y) * (C.x - A.x) == 0. Using cross products keeps everything in integers β no division, no float error.
from typing import List
class Solution:
def maxPoints(self, points: List[List[int]]) -> int:
n = len(points)
if n <= 2:
return n
best = 2
for i in range(n):
for j in range(i + 1, n):
ax, ay = points[i]
bx, by = points[j]
count = 2
for k in range(n):
if k == i or k == j:
continue
cx, cy = points[k]
cross = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax)
if cross == 0:
count += 1
best = max(best, count)
return best
Complexity: O(nΒ³) time, O(1) space. With n = 300 thatβs ~2.7Γ10β· inner iterations times the pair loop β actually 300Β³ β 2.7Γ10β·β¦ the pair loop makes it ~nΒ³/2 β 1.3Γ10β·Β·300, i.e. borderline. Itβs correct but redundantly re-derives each line from every pair on it.
Approach 2 β Anchor + slope hashing
The insight: collinear points through a fixed anchor all share the same slope relative to that anchor. So fix each point as an anchor, compute the slope to every other point, and count identical slopes with a hash map. The largest count for an anchor, plus the anchor itself, is the biggest line through it. Taking the max over all anchors is the answer β and each line of size k is rediscovered from each of its points, which is fine.
The one subtlety is representing slope exactly. Floating dy/dx loses precision, so we store the slope as a reduced integer pair (dy // g, dx // g) where g = gcd(dy, dx), normalized to a canonical sign so that, e.g., (1, 2) and (-1, -2) map to the same key.
from typing import List
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
best = 1
for i in range(n):
ax, ay = points[i]
slopes = defaultdict(int)
for j in range(n):
if j == i:
continue
dx = points[j][0] - ax
dy = points[j][1] - ay
if dx == 0: # vertical line
key = ("inf", 0)
elif dy == 0: # horizontal line
key = (0, "inf")
else:
g = gcd(dx, dy)
dx //= g
dy //= g
if dx < 0: # canonical sign: keep dx > 0
dx, dy = -dx, -dy
key = (dy, dx)
slopes[key] += 1
best = max(best, slopes[key] + 1) # + 1 for the anchor
return best
Walkthrough with points = [[1,1],[2,2],[3,3]]:
- Anchor
(1,1):
- to
(2,2): dx=1, dy=1, gcd=1, key (1, 1) β count 1, best = 2.
- to
(3,3): dx=2, dy=2, gcd=2 β (1, 1) β count 2, best = 3.
- Anchors
(2,2) and (3,3) also find slope (1,1) shared by the other two, confirming 3.
- Answer:
3.
Why the sign convention matters: from anchor (2,2), point (1,1) gives dx=-1, dy=-1, and point (3,3) gives dx=1, dy=1. Without normalizing to dx > 0 these hash to different keys (β1,β1) vs (1,1) and the line splits into two buckets. Forcing dx > 0 collapses them to one.
Complexity: O(nΒ²) time (each anchor scans all points, gcd is effectively constant on bounded ints), O(n) space for the per-anchor map.
Common pitfalls
- Using floating-point slope
dy/dx: 1/3 vs 2/6 can differ in the last bit, silently splitting a line. Always reduce to an integer (dy, dx) pair with gcd.
- Forgetting the sign convention, so opposite directions along the same line hash differently.
- Mishandling vertical (
dx == 0) and horizontal (dy == 0) lines β give them their own sentinel keys so gcd(0, k) edge cases donβt collide.
- Returning the bucket size without
+1 for the anchor, undercounting every line by one.
- Not special-casing
n <= 2: with 0, 1, or 2 points the answer is just n.
Pattern takeaway
For collinearity, anchor-and-slope turns a geometric question into counting equal keys β but only if the key is exact. Reduce direction vectors by their gcd and pin a sign convention so equal lines share one canonical key. The broader lesson: when floats threaten precision, re-express the invariant with integer arithmetic (reduced fractions, or cross-product zero tests).