Coding Interview Patterns

Course Content

Coding Interview Patterns

20 sections · 146 lessons

Max Points on a Line


Max Points on a Line is the classic hard geometry question. The algorithm is a hash map — the same "group by a key" idea as Group Anagrams. The difficulty is the key: a slope stored as a float breaks in ways that are hard to see, and the fix is a small piece of number theory. It is a good test of whether you think about exactness.

Stay inside integer arithmeticFloats invite bugs• Slope as dy divided by dx• A vertical line divides by zero• Equality on floats is unreliableIntegers stay exact• Compare squared distances, never roots• Collinear when the cross product is 0• Reduce dy and dx by their gcd as a key
Every trap in this lesson is avoided by the same habit: never leave exact integer arithmetic.

The problem

You get a list of points on a plane, each with integer coordinates [x, y]. Return the largest number of these points that lie on one straight line.

Example 1. [[1, 1], [2, 2], [3, 3]] → 3. All three are on the line y = x.

Example 2. [[1, 1], [3, 2], [5, 3], [4, 1], [2, 3], [1, 4]] → 4. The points (4, 1), (3, 2), (2, 3) and (1, 4) lie on the line x + y = 5.

Constraints. 1 ≤ number of points ≤ 300, coordinates between −10⁴ and 10⁴, and all points are distinct.

Clarifying questions

  • Can two points be identical? No, all distinct. (Duplicates are a follow-up.)
  • Integer coordinates? Yes — which makes exact arithmetic possible.
  • What if there are one or two points? The answer is 1 or 2: any two points share a line.
  • Vertical lines? Yes, possible, and a classic source of bugs.

Approach 1: the simple way

Every line through two or more points is fixed by some pair of those points. So try every pair, and for each pair count how many points lie on the line through it. Test collinearity with the cross product from the core lesson — exact, and no division.

Python
def cross(o: list[int], a: list[int], b: list[int]) -> int:    return (a[0] - o[0]) * (b[1] - o[1]) - (a[1] - o[1]) * (b[0] - o[0])def max_points_brute(points: list[list[int]]) -> int:    """Try every pair; count points collinear with it. O(n^3)."""    n = len(points)    if n <= 2:        return n    best = 2    for i in range(n):        for j in range(i + 1, n):            count = 2            for k in range(n):                if k != i and k != j and cross(points[i], points[j], points[k]) == 0:                    count += 1            best = max(best, count)    return best

O(n³) time, O(1) space. For 300 points that is about 13 million cross products; measured in Python, it took 2.0 seconds. It is correct, but too slow for most judges, and the same line is counted again for every pair on it.

The key insight

Fix one point, the anchor. Every other point defines a line through the anchor, and two points lie on the same line through the anchor exactly when they have the same slope from it. So group the other points by slope with a hash map. The biggest group, plus the anchor, is the best line through that anchor. Do this for each anchor: O(n²) pairs.

The whole problem is now: what is the hash key for a slope?

A float dy / dx fails in two ways. Vertical lines divide by zero. And floats round: two different slopes can round to the same float. With the points (0, 0), (94911151, 94911150) and (94911152, 94911151), the two slopes from (0, 0) are 94911150/94911151 and 94911151/94911152. They differ, but both round to the same float, so a float-keyed version returned 3 when the true answer is 2.

The exact key is the fraction itself, in lowest terms: (dx / g, dy / g) where g = gcd(dx, dy). Then (2, 1) and (4, 2) both become (2, 1). One more rule: the same line can give (1, −1) or (−1, 1), depending on which side of the anchor the point is. Normalise the sign — make dx positive, and if dx is 0, make dy positive — so both become (1, −1).

Approach 2: slopes as reduced fractions

Python
from collections import defaultdictfrom math import gcddef max_points(points: list[list[int]]) -> int:    """For each anchor, group later points by exact reduced slope. O(n^2 log C)."""    n = len(points)    if n <= 2:        return n    best = 0    for i in range(n):        x1, y1 = points[i]        slopes: dict[tuple[int, int], int] = defaultdict(int)        for j in range(i + 1, n):            x2, y2 = points[j]            dx, dy = x2 - x1, y2 - y1            g = gcd(dx, dy)                   # never 0: points are distinct            dx, dy = dx // g, dy // g            if dx < 0 or (dx == 0 and dy < 0):                dx, dy = -dx, -dy             # one key per direction            slopes[(dx, dy)] += 1            best = max(best, slopes[(dx, dy)])    return best + 1                           # add the anchor itself

Step by step:

  1. For each anchor i, start an empty map from slope to count.
  2. For each later point j, compute the difference, reduce it by the GCD, and normalise the sign.
  3. Count the key and track the best count seen.
  4. Return best + 1 for the anchor.

Only later points are compared (j > i). Any line's points are all counted when its first point is the anchor, so looking back would only repeat work.

Dry run on Example 2. Anchor (1, 1) first:

pointdx, dygcdkey after reducing and normalisingcount for key
(3, 2)2, 11(2, 1)1
(5, 3)4, 22(2, 1)2
(4, 1)3, 03(1, 0)1
(2, 3)1, 21(1, 2)1
(1, 4)0, 33(0, 1)1

Best so far is 2: the line through (1, 1), (3, 2), (5, 3), which is 3 points. Next anchor (3, 2):

pointdx, dygcdkeycount for key
(5, 3)2, 11(2, 1)1
(4, 1)1, −11(1, −1)1
(2, 3)−1, 11(1, −1) after flipping the sign2
(1, 4)−2, 22(1, −1) after reducing and flipping3

Best is now 3, so the answer is 3 + 1 = 4. The sign rule was essential: without it, (4, 1) and (2, 3) would get different keys, (1, −1) and (−1, 1), and the answer would come out as 3. The remaining anchors do not beat 3.

Complexity. O(n² log C) time, where C is the coordinate range (here 2 × 10⁴): there are n(n − 1)/2 pairs, and each GCD takes O(log C) steps. The old version of this course wrote O(n² log n); the log comes from the size of the coordinates, not the number of points. O(n) space for one anchor's map. Measured on 300 random points: 0.02 seconds, against 2.0 seconds for the brute force. Both versions matched on 300 random point sets.

Edge cases

  • One or two points. Returned directly: 1 or 2.
  • Vertical lines. dx = 0, so the key is (0, 1) after normalising — a normal key, no division. [[0, 0], [0, 1], [0, -1], [1, 5]] returns 3.
  • Horizontal lines. dy = 0; gcd(dx, 0) = |dx|, so the key is (1, 0).
  • Negative coordinates. The differences can be negative; math.gcd returns a non-negative value, and the sign rule makes the key consistent.

Follow-ups

  • "Points may repeat." Duplicates of the anchor lie on every line through it. Count them separately (dx = dy = 0 means a duplicate) and add that count to the anchor's best group, instead of hashing the key (0, 0).
  • "Do three given points lie on one line?" One cross product: cross(p1, p2, p3) == 0. O(1), exact.
  • "Find the convex hull of the points." Sort the points, then build the hull keeping only left turns — the sign of the same cross product (Andrew's monotone chain), O(n log n).

Check your understanding

0 of 2 answered

1.From anchor (0, 0), the points (4, −2) and (−6, 3) are compared. What keys do they get after reducing and normalising?

2.Why does the float key dy / dx fail on the points (0, 0), (94911151, 94911150), (94911152, 94911151)?