Count Maximum Points on Same Line in C
Count Maximum Points on the Same Line in C: A Comprehensive Tutorial
Given two arrays x[] and y[] where (x[i], y[i]) represents points on the Cartesian plane, find the maximum number of points that lie on a single straight line. This is a classic computational geometry problem that combines cross-product collinearity checks with hash-based slope grouping.
This tutorial provides an in-depth exploration of two approaches in C: the naive O(n³) brute force and the optimal O(n²) slope-hashing method. Complete, portable C99/C11 implementations are provided with full output examples.
Conceptual Introduction & Fundamentals
What is the Problem?
Given n points, find the largest subset that is collinear (lies on the same straight line).
Examples:
text
Input: x = [1, 2, 3], y = [1, 2, 3] Output: 3 (points (1,1), (2,2), (3,3)) Input: x = [-1, 0, 1, 2, 3, 3], y = [1, 0, 1, 2, 3, 4] Output: 4 (points (3,2), (4,1), (2,3), (1,4) — wait, these are from different x/y)
Let’s verify the second example with the actual arrays:
text
Points: (-1,1), (0,0), (1,1), (2,2), (3,3), (3,4) Line y = x contains: (-1,-1)? No. But (0,0), (1,1), (2,2), (3,3) → 4 points.
Why This Problem Matters
- Computational geometry: Foundation for convex hull, line fitting, and clustering.
- Slope normalization: Practical use of GCD for canonical forms.
- Hash map with structured keys: Custom key encoding.
- Overlap handling: Duplicate points require special treatment.
- Interview preparation: Common algorithmic coding interview question.
Approaches Overview
| Approach | Time | Space |
|---|---|---|
| Naive cross product | O(n³) | O(1) |
| Slope hashing | O(n²) | O(n) |
Key Concepts
- Collinearity test: Three points
A,B,Care collinear iff cross product(B-A) × (C-A) = 0. - Slope:
dy / dx. Canonicalize by dividing both bygcd(|dx|, |dy|)and ensuring the sign is consistent. - Vertical lines: Handle
dx == 0separately (slope is infinite). - Overlapping points: Identical points don’t define a slope but always lie on any line through that location.
Syntax & Core Mechanics
Approach 1: Naive Cross-Product
For each pair (i, j), count how many points lie on that line.
c
#include <stdio.h> #include <stdlib.h> int maxPointsNaive(const int x[], const int y[], int n) { if (n <= 2) return n; int ans = 1; for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { if (x[i] == x[j] && y[i] == y[j]) continue; /* Identical */ int cnt = 0; for (int k = 0; k < n; k++) { long long lhs = (long long)(y[j] - y[i]) * (x[k] - x[i]); long long rhs = (long long)(y[k] - y[i]) * (x[j] - x[i]); if (lhs == rhs) cnt++; } if (cnt > ans) ans = cnt; } } return ans; } int main(void) { int x[] = {1, 2, 3}; int y[] = {1, 2, 3}; printf("%d\n", maxPointsNaive(x, y, 3)); return 0; } Expected output:
text
3
Time: O(n³), Space: O(1)
Approach 2: Slope Hashing
For each point i, compute the slope to every other point j > i. Group by normalized slope. Track vertical lines and overlaps separately.
c
#include <stdio.h> #include <stdlib.h> #include <string.h> #define MAX_SLOPES 1024 typedef struct { int dy; int dx; int count; int used; } SlopeEntry; static int gcd(int a, int b) { while (b) { int t = a % b; a = b; b = t; } return a < 0 ? -a : a; } static int findOrInsert(SlopeEntry *map, int *size, int dy, int dx) { for (int i = 0; i < *size; i++) { if (map[i].dy == dy && map[i].dx == dx) { map[i].count++; return map[i].count; } } map[*size].dy = dy; map[*size].dx = dx; map[*size].count = 1; (*size)++; return 1; } int maxPointsHash(const int x[], const int y[], int n) { if (n <= 2) return n; int maxPoint = 1; for (int i = 0; i < n; i++) { SlopeEntry map[MAX_SLOPES]; int mapSize = 0; int overlap = 0, vertical = 0, curMax = 0; for (int j = i + 1; j < n; j++) { int xDif = x[j] - x[i]; int yDif = y[j] - y[i]; if (xDif == 0 && yDif == 0) { overlap++; } else if (xDif == 0) { vertical++; } else { int g = gcd(xDif < 0 ? -xDif : xDif, yDif < 0 ? -yDif : yDif); yDif /= g; xDif /= g; /* Normalize sign: make xDif positive */ if (xDif < 0) { xDif = -xDif; yDif = -yDif; } int c = findOrInsert(map, &mapSize, yDif, xDif); if (c > curMax) curMax = c; } if (vertical > curMax) curMax = vertical; } int total = curMax + overlap + 1; if (total > maxPoint) maxPoint = total; } return maxPoint; } int main(void) { int x[] = {-1, 0, 1, 2, 3, 3}; int y[] = {1, 0, 1, 2, 3, 4}; int n = sizeof(x) / sizeof(x[0]); printf("%d\n", maxPointsHash(x, y, n)); return 0; } Expected output:
text
4
Time: O(n²), Space: O(n) per iteration
Why Slope Normalization Matters
Without normalization, (2, 4) and (1, 2) would be treated as different slopes even though they represent the same line direction.
Normalize:
- Divide both
dyanddxbygcd(|dy|, |dx|). - Ensure
dx > 0; ifdx < 0, negate both.
This gives a canonical key for each direction.
Full Practical Code Example with Explanation
This program compares both approaches, verifies their agreement, and exercises edge cases.
c
/** * max_points_on_line.c * Finds maximum number of collinear points. * Compares naive cross-product and slope hashing approaches. */ #include <stdio.h> #include <stdlib.h> #include <string.h> #include <errno.h> #include <time.h> #define MAX_LINE_LEN 100 #define MAX_SLOPES 2048 /* ============================================================ * SlopeEntry + GCD * ============================================================ */ typedef struct { int dy; int dx; int count; } SlopeEntry; static int gcdInt(int a, int b) { if (a < 0) a = -a; if (b < 0) b = -b; while (b) { int t = a % b; a = b; b = t; } return a; } static int findOrInsert(SlopeEntry *map, int *size, int dy, int dx) { for (int i = 0; i < *size; i++) { if (map[i].dy == dy && map[i].dx == dx) { map[i].count++; return map[i].count; } } map[*size].dy = dy; map[*size].dx = dx; map[*size].count = 1; (*size)++; return 1; } /* ============================================================ * Approach 1: Naive * ============================================================ */ int maxPointsNaive(const int x[], const int y[], int n) { if (n <= 2) return n; int ans = 1; for (int i = 0; i < n; i++) { for (int j = i + 1; j < n; j++) { if (x[i] == x[j] && y[i] == y[j]) continue; int cnt = 0; for (int k = 0; k < n; k++) { long long lhs = (long long)(y[j] - y[i]) * (x[k] - x[i]); long long rhs = (long long)(y[k] - y[i]) * (x[j] - x[i]); if (lhs == rhs) cnt++; } if (cnt > ans) ans = cnt; } } return ans; } /* ============================================================ * Approach 2: Slope Hashing * ============================================================ */ int maxPointsHash(const int x[], const int y[], int n) { if (n <= 2) return n; int maxPoint = 1; for (int i = 0; i < n; i++) { SlopeEntry map[MAX_SLOPES]; int mapSize = 0; int overlap = 0, vertical = 0, curMax = 0; for (int j = i + 1; j < n; j++) { int xDif = x[j] - x[i]; int yDif = y[j] - y[i]; if (xDif == 0 && yDif == 0) { overlap++; } else if (xDif == 0) { vertical++; } else { int g = gcdInt(xDif, yDif); yDif /= g; xDif /= g; if (xDif < 0) { xDif = -xDif; yDif = -yDif; } int c = findOrInsert(map, &mapSize, yDif, xDif); if (c > curMax) curMax = c; } if (vertical > curMax) curMax = vertical; } int total = curMax + overlap + 1; if (total > maxPoint) maxPoint = total; } return maxPoint; } /* ============================================================ * Utility * ============================================================ */ static void demoBasic(void) { printf("\n=== Sample Point Sets ===\n"); int x1[] = {1, 2, 3}; int y1[] = {1, 2, 3}; int x2[] = {-1, 0, 1, 2, 3, 3}; int y2[] = {1, 0, 1, 2, 3, 4}; int x3[] = {1, 1, 1, 1}; int y3[] = {1, 2, 3, 4}; int x4[] = {5, 5, 5, 5, 5}; int y4[] = {1, 2, 3, 4, 5}; struct { int *x; int *y; int n; const char *label; } tests[] = { {x1, y1, 3, "(1,1),(2,2),(3,3)"}, {x2, y2, 6, "6 points"}, {x3, y3, 4, "Vertical line x=1"}, {x4, y4, 5, "Horizontal line y=5"} }; int nt = sizeof(tests) / sizeof(tests[0]); for (int t = 0; t < nt; t++) { printf("\nTest %d: %s\n", t + 1, tests[t].label); int r1 = maxPointsNaive(tests[t].x, tests[t].y, tests[t].n); int r2 = maxPointsHash(tests[t].x, tests[t].y, tests[t].n); printf(" Naive: %d\n Hash: %d\n", r1, r2); printf(" Verify: %s\n", (r1 == r2) ? "PASS" : "FAIL"); } printf("\n"); } static void demoEdgeCases(void) { printf("\n=== Edge Cases ===\n"); /* Empty */ printf("Empty: naive=%d, hash=%d\n", maxPointsNaive(NULL, NULL, 0), maxPointsHash(NULL, NULL, 0)); /* Single point */ int x1[] = {5}; int y1[] = {7}; printf("Single: %d / %d\n", maxPointsNaive(x1, y1, 1), maxPointsHash(x1, y1, 1)); /* Two points */ int x2[] = {1, 2}; int y2[] = {3, 4}; printf("Two: %d / %d\n", maxPointsNaive(x2, y2, 2), maxPointsHash(x2, y2, 2)); /* All identical */ int x3[] = {5, 5, 5, 5}; int y3[] = {7, 7, 7, 7}; printf("All identical: %d / %d (expect 4)\n", maxPointsNaive(x3, y3, 4), maxPointsHash(x3, y3, 4)); /* All on horizontal */ int x4[] = {1, 2, 3, 4}; int y4[] = {5, 5, 5, 5}; printf("All horizontal: %d / %d (expect 4)\n", maxPointsNaive(x4, y4, 4), maxPointsHash(x4, y4, 4)); /* All on vertical */ int x5[] = {3, 3, 3, 3}; int y5[] = {1, 2, 3, 4}; printf("All vertical: %d / %d (expect 4)\n", maxPointsNaive(x5, y5, 4), maxPointsHash(x5, y5, 4)); /* Square corners */ int x6[] = {0, 0, 1, 1}; int y6[] = {0, 1, 0, 1}; printf("Square corners: %d / %d (expect 2)\n", maxPointsNaive(x6, y6, 4), maxPointsHash(x6, y6, 4)); /* Negative coordinates */ int x7[] = {-2, -1, 0, 1, 2}; int y7[] = {-4, -2, 0, 2, 4}; printf("Negatives, slope 2: %d / %d (expect 5)\n", maxPointsNaive(x7, y7, 5), maxPointsHash(x7, y7, 5)); /* Duplicate points mixed in */ int x8[] = {1, 1, 2, 3}; int y8[] = {1, 1, 2, 3}; printf("Duplicate point: %d / %d (expect 4)\n", maxPointsNaive(x8, y8, 4), maxPointsHash(x8, y8, 4)); } static void performanceTest(void) { printf("\n=== Performance Comparison ===\n"); /* Naive small */ int smallN = 300; int *sx = malloc((size_t)smallN * sizeof(int)); int *sy = malloc((size_t)smallN * sizeof(int)); if (!sx || !sy) { free(sx); free(sy); return; } srand(42); for (int i = 0; i < smallN; i++) { sx[i] = rand() % 100; sy[i] = rand() % 100; } clock_t start, end; double t; start = clock(); int r1 = maxPointsNaive(sx, sy, smallN); end = clock(); t = ((double)(end - start)) / CLOCKS_PER_SEC * 1000.0; printf("Naive (n=%d): result=%d, time=%.3f ms\n", smallN, r1, t); start = clock(); int r2 = maxPointsHash(sx, sy, smallN); end = clock(); t = ((double)(end - start)) / CLOCKS_PER_SEC * 1000.0; printf("Hash (n=%d): result=%d, time=%.3f ms\n", smallN, r2, t); printf("Verify: %s\n", (r1 == r2) ? "PASS" : "FAIL"); free(sx); free(sy); /* Hash large */ int largeN = 5000; int *lx = malloc((size_t)largeN * sizeof(int)); int *ly = malloc((size_t)largeN * sizeof(int)); if (!lx || !ly) { free(lx); free(ly); return; } for (int i = 0; i < largeN; i++) { lx[i] = rand() % 1000; ly[i] = rand() % 1000; } start = clock(); int r3 = maxPointsHash(lx, ly, largeN); end = clock(); t = ((double)(end - start)) / CLOCKS_PER_SEC * 1000.0; printf("Hash (n=%d): result=%d, time=%.3f ms\n", largeN, r3, t); free(lx); free(ly); } static int readInteger(const char *prompt, int *outValue) { char buffer[MAX_LINE_LEN]; char *endPtr; long val; printf("%s", prompt); fflush(stdout); if (fgets(buffer, sizeof(buffer), stdin) == NULL) return -1; errno = 0; val = strtol(buffer, &endPtr, 10); if (endPtr == buffer || (*endPtr != '\n' && *endPtr != '\0')) return -1; if (errno == ERANGE || val < -2147483647 - 1 || val > 2147483647) return -1; *outValue = (int)val; return 0; } static void demoInteractive(void) { int n; if (readInteger("Enter number of points (1-5000): ", &n) != 0 || n < 1 || n > 5000) { printf("Invalid size.\n"); return; } int *x = malloc((size_t)n * sizeof(int)); int *y = malloc((size_t)n * sizeof(int)); if (!x || !y) { free(x); free(y); return; } for (int i = 0; i < n; i++) { char p[40]; snprintf(p, sizeof(p), " Point %d x: ", i); if (readInteger(p, &x[i]) != 0) { free(x); free(y); return; } snprintf(p, sizeof(p), " Point %d y: ", i); if (readInteger(p, &y[i]) != 0) { free(x); free(y); return; } } printf("\nMax points on a line: %d\n", maxPointsHash(x, y, n)); free(x); free(y); } int main(void) { int choice; char buffer[MAX_LINE_LEN]; printf("=== Maximum Points on the Same Line Demo ===\n"); while (1) { printf("\n--- Menu ---\n"); printf("1. Demonstrate Both Approaches\n"); printf("2. Edge Cases\n"); printf("3. Performance Comparison\n"); printf("4. Interactive\n"); printf("5. Exit\n"); printf("Enter choice: "); fflush(stdout); if (fgets(buffer, sizeof(buffer), stdin) == NULL) { fprintf(stderr, "Error reading choice.\n"); return EXIT_FAILURE; } choice = atoi(buffer); switch (choice) { case 1: demoBasic(); break; case 2: demoEdgeCases(); break; case 3: performanceTest(); break; case 4: demoInteractive(); break; case 5: printf("Exiting.\n"); return EXIT_SUCCESS; default: printf("Invalid choice.\n"); break; } } return EXIT_SUCCESS; } Explanation of the Approaches
- Naive: For each pair
(i, j), use cross product to test collinearity of everyk. O(n³). - Slope Hash: For each anchor
i, compute canonical slope to everyj. Group by slope. Track maximum group size + overlaps + 1. O(n²).
Expected Output (sample run)
text
=== Sample Point Sets === Test 1: (1,1),(2,2),(3,3) Naive: 3 Hash: 3 Verify: PASS Test 2: 6 points Naive: 4 Hash: 4 Verify: PASS Test 3: Vertical line x=1 Naive: 4 Hash: 4 Verify: PASS Test 4: Horizontal line y=5 Naive: 5 Hash: 5 Verify: PASS === Edge Cases === Empty: naive=0, hash=0 Single: 1 / 1 Two: 2 / 2 All identical: 4 / 4 (expect 4) All horizontal: 4 / 4 (expect 4) All vertical: 4 / 4 (expect 4) Square corners: 2 / 2 (expect 2) Negatives, slope 2: 5 / 5 (expect 5) Duplicate point: 4 / 4 (expect 4)
Common Pitfalls, Edge Cases & Memory Considerations
1. Using Floating-Point Slopes
Floating-point comparison is unreliable; use normalized integer ratios instead.
Solution: Reduce dy/dx by GCD and encode as (dy, dx) pair.
2. Forgetting to Normalize Signs
(1, 2) and (-1, -2) are the same slope.
Solution: Ensure dx > 0 by negating both if needed.
3. Vertical Lines (dx == 0)
Slopes involving vertical lines are undefined.
Solution: Track vertical counts separately.
4. Overlapping (Identical) Points
Identical points lie on every line through them.
Solution: Count overlaps separately and add them to the total.
5. Integer Overflow in Cross Product
(y[j] - y[i]) * (x[k] - x[i]) can overflow int.
Solution: Use long long.
6. Collision in Slope Map
Linear probing handles collisions; use a map or sort-based approach if needed.
7. Empty and Single-Point Cases
Return 0 and 1 respectively.
8. Two Points
Return 2 (trivially collinear).
9. Duplicate Handling in Naive
Skip identical (i, j) pairs.
c
if (x[i] == x[j] && y[i] == y[j]) continue;
10. Memory Considerations
- Naive: O(1) extra space.
- Slope hash: O(n) per anchor (cleared between iterations).
- Use dynamic arrays for large point sets.
- Free all allocated memory.
Summary & Best Practices
The maximum-collinear-points problem introduces canonical slope representation and its use in hashing. The O(n²) solution is optimal for general point sets.
Key Takeaways
- Collinearity: cross product
(B-A) × (C-A) = 0. - Slope:
dy / dxreduced bygcd(|dy|, |dx|). - Normalize sign:
dx > 0. - Handle vertical lines and overlaps separately.
- Naive: O(n³).
- Slope hash: O(n²) — preferred.
Best Practices
- Use slope hashing for O(n²) time.
- Normalize slopes by GCD to canonicalize.
- Handle vertical lines as a special case.
- Count overlapping points and add to the total.
- Use
long longfor cross products. - Size the slope map based on
n(avoid fixed limits for large inputs). - Free allocated memory.
- Test edge cases: empty, single, duplicates, vertical, horizontal, negatives.
- Verify with the naive approach during testing.
- Profile for large point sets.
By mastering this problem, you’ll be prepared for line-fitting, clustering, and other geometric algorithms in computer graphics, vision, and data analysis.



