Max Points on a Line (LeetCode 149)
You're handed a list of 2D points, each entry points[i] = [xi, yi] marking a spot on the X-Y plane. Work out the size of the largest subset of those points that all sit on one common straight line.
Worked Examples
points = [[1,1],[2,2],[3,3]]3points = [[1,1],[3,2],[5,3],[4,1],[2,3],[1,4]]4⚖️Formal Constraints & Bounds
1 <= points.length <= 300points[i].length == 2-104 <= xi, yi <= 104All the
pointsare unique.
Core Insight
Floating-point division dy/dx suffers from precision issues. Instead, reduce (dy, dx) by their greatest common divisor (GCD) and store the coprime pair (dy // g, dx // g) as hash map keys.
Real-World Scenario & Production Applications
Computer vision Hough transforms, radar trajectory line detection, geospatial linear feature recognition.
Step-by-Step Execution Trace Table
| Anchor Point | Target Point | Slope Delta (dx, dy) | Coprime Reduction (dy // g, dx // g) | Slopes Map for Anchor | Local Collinear Max |
|---|---|---|---|---|---|
(1, 1) | (2, 2) | dx = 1, dy = 1 | gcd=1 \implies (1, 1) | {(1, 1): 1} | 2 points |
(1, 1) | (3, 3) | dx = 2, dy = 2 | gcd=2 \implies (1, 1) | {(1, 1): 2} | 3 points |
(1, 1) | (1, 4) | dx = 0, dy = 3 | Vertical line (1, 0) | {(1, 1): 2, (1, 0): 1} | 3 points |
The Math, Step by Step
1. The key that names a direction
Fix one point, the anchor . Any other point lies in some direction from it. Write that direction as the step from the anchor to the point:
Two points lie on the same line through the anchor when their steps point the same way or exactly opposite ways. The steps and and all describe the same line. The code needs one canonical key for each line: a single value that every point of that line produces, and no point of another line produces.
2. Building the key for one pair
The code builds the key in two steps.
First it divides both parts by , the greatest common divisor. Python's math.gcd always returns a non-negative number, and here because all points are different. After the division, the two parts have no common factor left. So and both become . The key stores first, as (dy // g, dx // g). The division is exact, because divides both parts.
Then it fixes the sign, because and are still different pairs. The rule makes the first part positive, or, when the first part is 0, makes the second part positive:
Key (dy // g, dx // g) before the sign fix | Kind of line | Key after the sign fix |
|---|---|---|
| first part | rising or falling | unchanged |
| first part | rising or falling | both parts negated |
| first part , second part | horizontal | unchanged, |
| first part , second part | horizontal | negated to |
A vertical line has , so and the key is or . The sign fix makes it . No division by zero ever happens.
3. Why equal keys mean one line
Two points with the same key have steps from the anchor that are whole multiples of the same reduced pair. So both lie on the one line through the anchor in that direction. Two points on different lines through the anchor have directions that are not multiples of each other. After dividing by the gcd and fixing the sign, their keys differ. A line through the anchor is fully fixed by one point and its direction, so counting equal keys counts the points on each line.
For each anchor, slopes[key] counts the other points on the line with that key. The line also holds the anchor itself, so it has slopes[key] + 1 points.
4. Why later points are enough, and the small inputs
The inner loop only looks at points with index . That is enough. Take the line with the most points, and let be the smallest index among them. When the anchor is point , all the other points of that line have larger indices, so they are all counted.
With one or two points, every point is on one line, so the code returns len(points) at once. The loops would return 0 for a single point, because they never run a pair.
5. Example 2 by hand
points = [[1,1],[3,2],[5,3],[4,1],[2,3],[1,4]].
The picture shows the six points, labelled by index. Points 1, 3, 4 and 5 are , , and . They lie on the line , which falls by 1 for every step of 1 to the right.
The smallest index on that line is 1, so the anchor finds it:
j | Point | (dy // g, dx // g) | Key after the sign fix | slopes[key] | |||
|---|---|---|---|---|---|---|---|
| 2 | 2 | 1 | 1 | 1 | |||
| 3 | 1 | 1 | 1 | ||||
| 4 | 1 | 1 | 2 | ||||
| 5 | 2 | 2 | 3 |
Row j = 3 shows why the sign fix is needed. Before it, gives and gives . Both points are on the same line, but their pairs differ. After the fix, both keys are . Row j = 5 shows why the gcd is needed. Its step is twice the step of row j = 4, and dividing by gives the same key.
After row j = 5, the key has count 3, so this line holds points. No anchor finds a larger count, so the code returns 4, the expected output.
6. Example 1 by hand
points = [[1,1],[2,2],[3,3]]. With the anchor , the point has step and key . The point has step . Its gcd is 2, so its key is also . The count for reaches 2, so the line holds points. The code returns 3, the expected output.
7. From the math to the code
| Math | In maxPoints |
|---|---|
| the step from the anchor | dx, dy = x2 - x1, y2 - y1 |
g = math.gcd(dx, dy) | |
| the reduced pair, first | slope = (dy // g, dx // g) |
| the sign fix | if slope[0] < 0 or (slope[0] == 0 and slope[1] < 0): then negate both parts |
| points on this line, plus the anchor | slopes[slope] += 1 and slopes[slope] + 1 |
| the largest line over all anchors | max_pts = max(max_pts, slopes[slope] + 1) |
The two loops look at every pair once, and each key takes one gcd. Under LeetCode's limits every coordinate difference is at most , so one gcd takes a bounded number of steps. So the code takes time. The map for one anchor holds at most keys, so it uses space.
| 1 | if len(points) <= 2: |
| 2 | return len(points) |
| 3 | max_pts = 0 |
| 4 | for i in range(len(points)): |
| 5 | slopes = defaultdict(int) |
| 6 | x1, y1 = points[i] |
| 7 | for j in range(i + 1, len(points)): |
| 8 | dx, dy = x2 - x1, y2 - y1 |
| 9 | g = math.gcd(dx, dy) |
| 10 | slope = (dy // g, dx // g), with one fixed sign |
| 11 | slopes[slope] += 1 |
| 12 | max_pts = max(max_pts, slopes[slope] + 1) |
| 13 | return max_pts |
Conceptual Narrative
🧭 Conceptual Foundation & Pattern Intuition
Grouping points by slope sounds like it should just be dy / dx in a hash map — until you remember floats lie.
📏 The Analogy: A Surveyor Who Never Trusts Decimals
Imagine a surveyor comparing the steepness of different paths. Two paths are "the same direction" only if their rise-over-run ratios are exactly equal — but a surveyor who measures steepness as a decimal (0.3333...) risks two genuinely-identical directions coming out as 0.33333 and 0.33334 due to rounding, and missing that they're the same line entirely. A careful surveyor never divides at all. Instead, they write the ratio as a fraction and simplify it down to its smallest possible whole-number form — 9/3 and 6/2 both simplify to 3/1, and now they're exactly the same key, no rounding involved.
🔑 The Key Idea in the Code
math.gcd(dx, dy) finds the largest whole number that divides both the rise and the run evenly. Dividing both by it collapses every direction to one, canonical, integer-pair key — (3, 1) and never 0.333.... Two points genuinely on the same line through the anchor will always reduce to the identical pair, so the hash map lookup is 100% reliable instead of "usually right."
Floating point slope division precision error: Computing slope as
dy / dxuses IEEE 754 floating-point numbers, which suffer precision drift on steep slopes. Normalize the fraction(dy // g, dx // g)using the greatest common divisor (GCD).Vertical line division by zero: When two points share the same x-coordinate (), division by zero crashes. Normalize vertical slopes to a fixed sentinel like
(1, 0).Duplicate points at identical coordinates: When points share the exact same coordinates, slope is undefined; track duplicate point counts separately and add to the maximum collinear line.
4-Phase Thought Process Model
Pattern Recognition Signals
The 10-second spot
Maximum points lying on the same straight line, geometry 2D coordinate plane, line slope collisions.
Formulating the Predicate & Invariants
Turning intuition into a boolean rule
Invariant: Two lines passing through the same anchor point i with identical reduced slopes (dy/gcd, dx/gcd) represent the exact same line.
Silent Failure Traps & Edge Cases
Where confident candidates still lose points
The card's trap, with the blueprint's rule: flip
slopewhenslope[0] < 0or (slope[0] == 0andslope[1] < 0), sodyends up positive (ordy == 0withdxpositive).Vertical and horizontal lines need no special case:
gcd(0, dy)isabs(dy), so a vertical line becomes(1, 0)after the sign fix, and a horizontal one becomes(0, 1).Count the anchor too: the line through
points[i]holdsslopes[slope] + 1points; without the+ 1every answer is one short.
The 60-Second Interview Pitch
Say this out loud before you type a single line
I'd use a Canonical Key. A line through two points is fixed by one anchor point and a direction, so for each anchor
points[i]I fill a fresh mapslopeswith the direction to every later pointpoints[j]. I write that direction as the reduced integer pair(dy // g, dx // g)withg = gcd(dx, dy), so directions such as(2, 4)and(1, 2)get the same key. Two points lie on the same line through the anchor exactly when their pairs match, and integers compare exactly, unlike rounded float slopes. The trap is the sign:(1, -2)and(-1, 2)are the same direction, so I flip the pair wheneverdyis negative, ordyis zero anddxis negative; otherwise one line is counted as two. The best line through the anchor hasslopes[slope] + 1points, counting the anchor itself. That's O(N²) time andO(N)space.
Complexity & Mathematical Proof
O(N^2)
Look at the execution steps: For each point i (N choices), computes normalized slope (dy, dx) via GCD with all other N - 1 points j. Hash map tracks slope frequencies. Max slope count gives max collinear points through point i. Total N * (N - 1) slope calculations.
O(N)
Look at memory allocations: Call stack uses O(1) Iterative. Hash map storing at most N - 1 reduced slope pairs per anchor point.
T(N) = ∑_{i=1}^N (N - i) = O(N^2)
Derivation Progression
N anchor points
Fixes point i as origin.
g = gcd(dx, dy), (dy // g, dx // g)
Reduces the slope with Euclid's algorithm. Differences are at most 2 * 10^4, so one gcd takes at most 20 remainder steps (the worst pair is 17711 and 10946): a constant under these limits, so each pair costs O(1). (With unbounded coordinates C, the total would be O(N^2 log C).)
slopes[(dy // g, dx // g)] += 1
Tally collinear lines through anchor point in O(1) map operation.
Variable Definitions
Total number of 2D points in the input array
Memory Architecture & Bounds
O(1) Iterative
O(N) Hash map of reduced slope fractions
O(1) Max points integer
Boundary Best / Worst Cases
for N <= 2.
.
general case.
Recurrence Tree Topology
Senior SWE Deconstruction & Hardware Caveats
Key phrasing: the largest count of points sharing one straight line. Geometry trigger: slope collinearity. Points are collinear if . The fatal junior trap is computing float(dy) / dx: floating-point division introduces IEEE-754 precision drift and rounding errors, causing identical slopes to hash to different buckets. A Senior SWE immediately implements Coprime GCD Slope Normalization: represent the slope as a canonical coprime integer pair .
. With , an pairwise slope aggregation using a local hash map takes pair checks.
Edge case: immediately returns . At scale, memory stays small because slopes is rebuilt for each anchor: it never holds more than keys, although pairs are examined in total.
Max Points on a Line (LeetCode 149)
You're handed a list of 2D points, each entry points[i] = [xi, yi] marking a spot on the X-Y plane. Work out the size of the largest subset of those points that all sit on one common straight line.
Worked Examples
points = [[1,1],[2,2],[3,3]]3points = [[1,1],[3,2],[5,3],[4,1],[2,3],[1,4]]4⚖️Formal Constraints & Bounds
1 <= points.length <= 300points[i].length == 2-104 <= xi, yi <= 104All the
pointsare unique.
Core Insight
Floating-point division dy/dx suffers from precision issues. Instead, reduce (dy, dx) by their greatest common divisor (GCD) and store the coprime pair (dy // g, dx // g) as hash map keys.
Real-World Scenario & Production Applications
Computer vision Hough transforms, radar trajectory line detection, geospatial linear feature recognition.
Step-by-Step Execution Trace Table
| Anchor Point | Target Point | Slope Delta (dx, dy) | Coprime Reduction (dy // g, dx // g) | Slopes Map for Anchor | Local Collinear Max |
|---|---|---|---|---|---|
(1, 1) | (2, 2) | dx = 1, dy = 1 | gcd=1 \implies (1, 1) | {(1, 1): 1} | 2 points |
(1, 1) | (3, 3) | dx = 2, dy = 2 | gcd=2 \implies (1, 1) | {(1, 1): 2} | 3 points |
(1, 1) | (1, 4) | dx = 0, dy = 3 | Vertical line (1, 0) | {(1, 1): 2, (1, 0): 1} | 3 points |
The Math, Step by Step
1. The key that names a direction
Fix one point, the anchor . Any other point lies in some direction from it. Write that direction as the step from the anchor to the point:
Two points lie on the same line through the anchor when their steps point the same way or exactly opposite ways. The steps and and all describe the same line. The code needs one canonical key for each line: a single value that every point of that line produces, and no point of another line produces.
2. Building the key for one pair
The code builds the key in two steps.
First it divides both parts by , the greatest common divisor. Python's math.gcd always returns a non-negative number, and here because all points are different. After the division, the two parts have no common factor left. So and both become . The key stores first, as (dy // g, dx // g). The division is exact, because divides both parts.
Then it fixes the sign, because and are still different pairs. The rule makes the first part positive, or, when the first part is 0, makes the second part positive:
Key (dy // g, dx // g) before the sign fix | Kind of line | Key after the sign fix |
|---|---|---|
| first part | rising or falling | unchanged |
| first part | rising or falling | both parts negated |
| first part , second part | horizontal | unchanged, |
| first part , second part | horizontal | negated to |
A vertical line has , so and the key is or . The sign fix makes it . No division by zero ever happens.
3. Why equal keys mean one line
Two points with the same key have steps from the anchor that are whole multiples of the same reduced pair. So both lie on the one line through the anchor in that direction. Two points on different lines through the anchor have directions that are not multiples of each other. After dividing by the gcd and fixing the sign, their keys differ. A line through the anchor is fully fixed by one point and its direction, so counting equal keys counts the points on each line.
For each anchor, slopes[key] counts the other points on the line with that key. The line also holds the anchor itself, so it has slopes[key] + 1 points.
4. Why later points are enough, and the small inputs
The inner loop only looks at points with index . That is enough. Take the line with the most points, and let be the smallest index among them. When the anchor is point , all the other points of that line have larger indices, so they are all counted.
With one or two points, every point is on one line, so the code returns len(points) at once. The loops would return 0 for a single point, because they never run a pair.
5. Example 2 by hand
points = [[1,1],[3,2],[5,3],[4,1],[2,3],[1,4]].
The picture shows the six points, labelled by index. Points 1, 3, 4 and 5 are , , and . They lie on the line , which falls by 1 for every step of 1 to the right.
The smallest index on that line is 1, so the anchor finds it:
j | Point | (dy // g, dx // g) | Key after the sign fix | slopes[key] | |||
|---|---|---|---|---|---|---|---|
| 2 | 2 | 1 | 1 | 1 | |||
| 3 | 1 | 1 | 1 | ||||
| 4 | 1 | 1 | 2 | ||||
| 5 | 2 | 2 | 3 |
Row j = 3 shows why the sign fix is needed. Before it, gives and gives . Both points are on the same line, but their pairs differ. After the fix, both keys are . Row j = 5 shows why the gcd is needed. Its step is twice the step of row j = 4, and dividing by gives the same key.
After row j = 5, the key has count 3, so this line holds points. No anchor finds a larger count, so the code returns 4, the expected output.
6. Example 1 by hand
points = [[1,1],[2,2],[3,3]]. With the anchor , the point has step and key . The point has step . Its gcd is 2, so its key is also . The count for reaches 2, so the line holds points. The code returns 3, the expected output.
7. From the math to the code
| Math | In maxPoints |
|---|---|
| the step from the anchor | dx, dy = x2 - x1, y2 - y1 |
g = math.gcd(dx, dy) | |
| the reduced pair, first | slope = (dy // g, dx // g) |
| the sign fix | if slope[0] < 0 or (slope[0] == 0 and slope[1] < 0): then negate both parts |
| points on this line, plus the anchor | slopes[slope] += 1 and slopes[slope] + 1 |
| the largest line over all anchors | max_pts = max(max_pts, slopes[slope] + 1) |
The two loops look at every pair once, and each key takes one gcd. Under LeetCode's limits every coordinate difference is at most , so one gcd takes a bounded number of steps. So the code takes time. The map for one anchor holds at most keys, so it uses space.
| 1 | if len(points) <= 2: |
| 2 | return len(points) |
| 3 | max_pts = 0 |
| 4 | for i in range(len(points)): |
| 5 | slopes = defaultdict(int) |
| 6 | x1, y1 = points[i] |
| 7 | for j in range(i + 1, len(points)): |
| 8 | dx, dy = x2 - x1, y2 - y1 |
| 9 | g = math.gcd(dx, dy) |
| 10 | slope = (dy // g, dx // g), with one fixed sign |
| 11 | slopes[slope] += 1 |
| 12 | max_pts = max(max_pts, slopes[slope] + 1) |
| 13 | return max_pts |
Conceptual Narrative
🧭 Conceptual Foundation & Pattern Intuition
Grouping points by slope sounds like it should just be dy / dx in a hash map — until you remember floats lie.
📏 The Analogy: A Surveyor Who Never Trusts Decimals
Imagine a surveyor comparing the steepness of different paths. Two paths are "the same direction" only if their rise-over-run ratios are exactly equal — but a surveyor who measures steepness as a decimal (0.3333...) risks two genuinely-identical directions coming out as 0.33333 and 0.33334 due to rounding, and missing that they're the same line entirely. A careful surveyor never divides at all. Instead, they write the ratio as a fraction and simplify it down to its smallest possible whole-number form — 9/3 and 6/2 both simplify to 3/1, and now they're exactly the same key, no rounding involved.
🔑 The Key Idea in the Code
math.gcd(dx, dy) finds the largest whole number that divides both the rise and the run evenly. Dividing both by it collapses every direction to one, canonical, integer-pair key — (3, 1) and never 0.333.... Two points genuinely on the same line through the anchor will always reduce to the identical pair, so the hash map lookup is 100% reliable instead of "usually right."
Floating point slope division precision error: Computing slope as
dy / dxuses IEEE 754 floating-point numbers, which suffer precision drift on steep slopes. Normalize the fraction(dy // g, dx // g)using the greatest common divisor (GCD).Vertical line division by zero: When two points share the same x-coordinate (), division by zero crashes. Normalize vertical slopes to a fixed sentinel like
(1, 0).Duplicate points at identical coordinates: When points share the exact same coordinates, slope is undefined; track duplicate point counts separately and add to the maximum collinear line.
4-Phase Thought Process Model
Pattern Recognition Signals
The 10-second spot
Maximum points lying on the same straight line, geometry 2D coordinate plane, line slope collisions.
Formulating the Predicate & Invariants
Turning intuition into a boolean rule
Invariant: Two lines passing through the same anchor point i with identical reduced slopes (dy/gcd, dx/gcd) represent the exact same line.
Silent Failure Traps & Edge Cases
Where confident candidates still lose points
The card's trap, with the blueprint's rule: flip
slopewhenslope[0] < 0or (slope[0] == 0andslope[1] < 0), sodyends up positive (ordy == 0withdxpositive).Vertical and horizontal lines need no special case:
gcd(0, dy)isabs(dy), so a vertical line becomes(1, 0)after the sign fix, and a horizontal one becomes(0, 1).Count the anchor too: the line through
points[i]holdsslopes[slope] + 1points; without the+ 1every answer is one short.
The 60-Second Interview Pitch
Say this out loud before you type a single line
I'd use a Canonical Key. A line through two points is fixed by one anchor point and a direction, so for each anchor
points[i]I fill a fresh mapslopeswith the direction to every later pointpoints[j]. I write that direction as the reduced integer pair(dy // g, dx // g)withg = gcd(dx, dy), so directions such as(2, 4)and(1, 2)get the same key. Two points lie on the same line through the anchor exactly when their pairs match, and integers compare exactly, unlike rounded float slopes. The trap is the sign:(1, -2)and(-1, 2)are the same direction, so I flip the pair wheneverdyis negative, ordyis zero anddxis negative; otherwise one line is counted as two. The best line through the anchor hasslopes[slope] + 1points, counting the anchor itself. That's O(N²) time andO(N)space.
Complexity & Mathematical Proof
O(N^2)
Look at the execution steps: For each point i (N choices), computes normalized slope (dy, dx) via GCD with all other N - 1 points j. Hash map tracks slope frequencies. Max slope count gives max collinear points through point i. Total N * (N - 1) slope calculations.
O(N)
Look at memory allocations: Call stack uses O(1) Iterative. Hash map storing at most N - 1 reduced slope pairs per anchor point.
T(N) = ∑_{i=1}^N (N - i) = O(N^2)
Derivation Progression
N anchor points
Fixes point i as origin.
g = gcd(dx, dy), (dy // g, dx // g)
Reduces the slope with Euclid's algorithm. Differences are at most 2 * 10^4, so one gcd takes at most 20 remainder steps (the worst pair is 17711 and 10946): a constant under these limits, so each pair costs O(1). (With unbounded coordinates C, the total would be O(N^2 log C).)
slopes[(dy // g, dx // g)] += 1
Tally collinear lines through anchor point in O(1) map operation.
Variable Definitions
Total number of 2D points in the input array
Memory Architecture & Bounds
O(1) Iterative
O(N) Hash map of reduced slope fractions
O(1) Max points integer
Boundary Best / Worst Cases
for N <= 2.
.
general case.
Recurrence Tree Topology
Senior SWE Deconstruction & Hardware Caveats
Key phrasing: the largest count of points sharing one straight line. Geometry trigger: slope collinearity. Points are collinear if . The fatal junior trap is computing float(dy) / dx: floating-point division introduces IEEE-754 precision drift and rounding errors, causing identical slopes to hash to different buckets. A Senior SWE immediately implements Coprime GCD Slope Normalization: represent the slope as a canonical coprime integer pair .
. With , an pairwise slope aggregation using a local hash map takes pair checks.
Edge case: immediately returns . At scale, memory stays small because slopes is rebuilt for each anchor: it never holds more than keys, although pairs are examined in total.