Facebook Pixel

3588. Find Maximum Area of a Triangle

MediumGreedyGeometryArrayHash TableMathEnumeration
LeetCode ↗

Problem Description

You are given a 2D array coords of size n x 2, where each element represents the coordinates of a point in an infinite Cartesian plane. In other words, coords contains n points, and each point has an x-coordinate and a y-coordinate.

Your task is to find a triangle whose three corners are chosen from any three points in coords, subject to one special condition: at least one side of this triangle must be parallel to the x-axis or the y-axis.

  • A side is parallel to the x-axis if its two endpoints share the same y-coordinate.
  • A side is parallel to the y-axis if its two endpoints share the same x-coordinate.

Among all valid triangles satisfying this condition, you need to find the one with the maximum area. Let this maximum area be A. The problem asks you to return 2 * A (that is, twice the maximum area).

If no such triangle exists, return -1.

Note that a triangle cannot have zero area, so the three chosen points must not be collinear and must form a proper triangle.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

How We Pick the Algorithm

Why Greedy Algorithms?

This problem maps to Greedy Algorithms through a short path in the full flowchart.

Computemax/min?yesGreedyselection?yesGreedyAlgorithms

The problem greedily picks the longest vertical base per x-coordinate and the farthest horizontal third point, then swaps axes for the horizontal case.

Open in Flowchart

Intuition

The key constraint is that at least one side of the triangle must be parallel to the x-axis or y-axis. This greatly simplifies how we think about the problem, because the area of a triangle can be computed as (base * height) / 2. If we fix one side as the base, the area only depends on the length of that base and the perpendicular distance from the third point to the base.

Let's first focus on triangles that have a side parallel to the y-axis. A side parallel to the y-axis means its two endpoints share the same x-coordinate. So for any fixed x-coordinate, we can look at all the points sitting on that vertical line. The longest possible vertical segment (the base) on that line is formed by the point with the maximum y-coordinate and the point with the minimum y-coordinate. Its length is max_y - min_y.

Now, with this vertical base fixed at x-coordinate x, the height of the triangle is just the horizontal distance from the third point to this vertical line, which is |x_third - x|. To maximize the area, we want this horizontal distance to be as large as possible. That distance is maximized by choosing the third point with either the smallest x-coordinate or the largest x-coordinate among all points. So the best height for base at x is max(mx - x, x - mn), where mn and mx are the global minimum and maximum x-coordinates.

This leads to a clean strategy:

  • Group points by their x-coordinate, recording the minimum and maximum y for each group (stored in hash maps f and g).
  • Also track the global minimum x (mn) and maximum x (mx).
  • For each x-coordinate, the doubled area is (g[x] - f[x]) * max(mx - x, x - mn). We take the largest such value.

Since the problem wants 2 * A, we conveniently compute base * height directly without dividing by 2.

This handles triangles with a side parallel to the y-axis. To handle triangles with a side parallel to the x-axis, we simply swap the x and y coordinates of every point and run the exact same procedure again. Swapping turns vertical sides into horizontal ones, so reusing the same calc function covers both cases.

Finally, we take the maximum result from both runs. If the answer is 0, it means no valid non-degenerate triangle exists, so we return -1.

Pattern Learn more about Greedy and Math patterns.

Solution Approach

We use Enumeration + Hash Map to solve this problem.

Since the problem asks for twice the area of the triangle, we directly compute the product of the base and height (base * height), avoiding the division by 2.

The triangle must have at least one side parallel to the x-axis or y-axis. So we design a helper function calc that computes the doubled area for all possible triangles whose base is parallel to the y-axis (i.e., a vertical side). Then, by swapping coordinates, we reuse the same function to handle bases parallel to the x-axis.

Inside the calc function:

  1. We maintain two hash maps:

    • f: records the minimum y-coordinate for each x-coordinate.
    • g: records the maximum y-coordinate for each x-coordinate.
  2. We also track two scalar values:

    • mn: the global minimum x-coordinate.
    • mx: the global maximum x-coordinate.
  3. We iterate through coords. For each point (x, y):

    • Update mn = min(mn, x) and mx = max(mx, x).
    • If x already exists in f, update f[x] = min(f[x], y) and g[x] = max(g[x], y).
    • Otherwise, initialize f[x] = g[x] = y.
  4. After collecting this information, we iterate through f. For each x-coordinate x with minimum y-value y:

    • The length of the vertical base is d = g[x] - y.
    • The maximum height is max(mx - x, x - mn), choosing the third point as far left or as far right as possible.
    • The doubled area candidate is d * max(mx - x, x - mn).
    • We update ans = max(ans, d * max(mx - x, x - mn)).
  5. Return ans.

Inside the main function:

  1. Call calc() once to get the best doubled area for triangles with a side parallel to the y-axis.

  2. Swap the x and y coordinates of every point in coords:

    for c in coords:
        c[0], c[1] = c[1], c[0]

    This transforms vertical sides into horizontal ones.

  3. Call calc() again and take the maximum: ans = max(ans, calc()). This now covers triangles with a side parallel to the x-axis.

  4. Finally, return ans if it is non-zero; otherwise return -1, since ans == 0 means no valid non-degenerate triangle exists.

Complexity Analysis:

  • Time complexity: O(n), where n is the number of points. We make a constant number of linear passes over coords (building the hash maps and iterating through them).
  • Space complexity: O(n), for the hash maps f and g that store at most one entry per distinct x-coordinate.

Example Walkthrough

Let's use a small concrete example to trace through the solution approach step by step.

Input:

coords = [[1, 1], [1, 4], [3, 2], [5, 1]]

We want to find 2 * A, where A is the maximum area of a triangle with at least one side parallel to an axis.


Pass 1 — calc() for sides parallel to the y-axis (vertical bases)

Step 1 — Build the hash maps and track global x bounds.

We scan every point (x, y), recording the min/max y per x-coordinate, and updating the global min/max x.

PointActionf (min y per x)g (max y per x)mnmx
(1,1)new x=1{1:1}{1:1}11
(1,4)update x=1{1:1}{1:4}11
(3,2)new x=3{1:1, 3:2}{1:4, 3:2}13
(5,1)new x=5{1:1, 3:2, 5:1}{1:4, 3:2, 5:1}15

After this pass: mn = 1, mx = 5.

Step 2 — Evaluate each vertical base.

For each x, the vertical base length is d = g[x] - f[x], and the best height is the farthest horizontal reach max(mx - x, x - mn).

  • x = 1: base d = 4 - 1 = 3. Height max(5-1, 1-1) = max(4, 0) = 4. Doubled area = 3 * 4 = 12.
  • x = 3: base d = 2 - 2 = 0. Only one point at x=3, so no vertical segment. Doubled area = 0 * ... = 0.
  • x = 5: base d = 1 - 1 = 0. Doubled area = 0.

Best from Pass 1: 12.

This corresponds to the vertical base between (1,1) and (1,4) (length 3), with the third point pushed as far right as possible to x = 5 (height 4). Area = 12 / 2 = 6.


Pass 2 — Swap coordinates, calc() for sides parallel to the x-axis

Step 3 — Swap x and y in every point.

coords becomes [[1, 1], [4, 1], [2, 3], [1, 5]]

A horizontal side (shared y) in the original now becomes a vertical side (shared x) in the swapped version, so we reuse the same logic.

Build hash maps again:

Pointf (min y per x)g (max y per x)mnmx
(1,1){1:1}{1:1}11
(4,1){1:1, 4:1}{1:1, 4:1}14
(2,3){1:1, 4:1, 2:3}{1:1, 4:1, 2:3}14
(1,5){1:1, 4:1, 2:3}{1:5, 4:1, 2:3}14

After this pass: mn = 1, mx = 4.

Evaluate each base:

  • x = 1: base d = 5 - 1 = 4. Height max(4-1, 1-1) = 3. Doubled area = 4 * 3 = 12.
  • x = 4: base d = 1 - 1 = 0. Doubled area = 0.
  • x = 2: base d = 3 - 3 = 0. Doubled area = 0.

Best from Pass 2: 12.

(In original coordinates, this represents the horizontal side between (1,1) and (1,5) after swapping — i.e., a triangle using a horizontal base — also yielding doubled area 12.)


Step 4 — Combine results

ans = max(12, 12) = 12

Since ans != 0, we return 12.


Verification: The winning triangle uses points (1,1), (1,4), and (5,1). The side from (1,1) to (1,4) is vertical (shared x = 1), length 3. The third point (5,1) is at horizontal distance 5 - 1 = 4. Area = (3 * 4) / 2 = 6, so 2 * A = 12. ✓

Solution Implementation

1from typing import List
2from math import inf
3
4
5class Solution:
6    def maxArea(self, coords: List[List[int]]) -> int:
7        # Compute the maximum triangle area achievable when the base of the
8        # triangle lies along a vertical line (i.e. shares the same x value).
9        # For each distinct x, the vertical "base" length is the spread between
10        # the maximum and minimum y values at that x. The "height" is the
11        # farthest horizontal distance from that x to either the global min x
12        # or the global max x.
13        def calc() -> int:
14            min_x, max_x = inf, 0          # Global minimum and maximum x values
15            lowest_y = {}                  # For each x: the smallest y seen
16            highest_y = {}                 # For each x: the largest y seen
17
18            # Record per-x extremes of y and track global x bounds
19            for x, y in coords:
20                min_x = min(min_x, x)
21                max_x = max(max_x, x)
22                if x in lowest_y:
23                    lowest_y[x] = min(lowest_y[x], y)
24                    highest_y[x] = max(highest_y[x], y)
25                else:
26                    lowest_y[x] = highest_y[x] = y
27
28            best = 0
29            # For each vertical base, the triangle area is:
30            #   (1/2) * base * height
31            # Here we accumulate base * height (twice the area) so the final
32            # area can be derived as a fraction without floating-point issues.
33            for x, y_min in lowest_y.items():
34                base = highest_y[x] - y_min                 # Vertical base length
35                height = max(max_x - x, x - min_x)          # Max horizontal reach
36                best = max(best, base * height)
37            return best
38
39        # First pass: triangles with a vertical base
40        ans = calc()
41
42        # Swap x and y coordinates so a second pass evaluates triangles
43        # whose base lies along a horizontal line instead.
44        for point in coords:
45            point[0], point[1] = point[1], point[0]
46        ans = max(ans, calc())
47
48        # `ans` currently holds twice the maximum area (base * height).
49        # Return it if a valid triangle exists, otherwise -1.
50        return ans if ans else -1
51
1class Solution {
2    /**
3     * Computes the maximum area of an axis-aligned right triangle that can be
4     * formed using the given points, where one leg of the triangle is parallel
5     * to one of the coordinate axes.
6     *
7     * The approach is run twice: once with the original coordinates, and once
8     * with x and y swapped. Swapping the axes lets us reuse the same logic
9     * (which fixes one axis and varies the other) to cover both orientations.
10     *
11     * @param coords array of points, each represented as [x, y]
12     * @return the maximum triangle area, or -1 if no valid triangle exists
13     */
14    public long maxArea(int[][] coords) {
15        // First pass: treat x as the "grouping" axis.
16        long ans = calc(coords);
17
18        // Swap x and y in every point so the second pass handles the other axis.
19        for (int[] c : coords) {
20            int tmp = c[0];
21            c[0] = c[1];
22            c[1] = tmp;
23        }
24
25        // Second pass with swapped coordinates.
26        ans = Math.max(ans, calc(coords));
27
28        // A valid (positive) area means a triangle exists; otherwise return -1.
29        return ans > 0 ? ans : -1;
30    }
31
32    /**
33     * Calculates the maximum triangle area for the current orientation.
34     *
35     * For each distinct x value, we look at the vertical extent (the difference
36     * between the maximum and minimum y at that x). This vertical segment forms
37     * one leg of the right triangle. The other leg is the horizontal distance
38     * from x to the farthest x boundary (either the global min or max x).
39     * The area is half the product of the two legs; since the formula keeps
40     * d * base, the factor of 1/2 is implicitly handled by the problem's
41     * definition of area being measured this way.
42     *
43     * @param coords array of points, each represented as [x, y]
44     * @return the maximum area found, or 0 if none
45     */
46    private long calc(int[][] coords) {
47        // Global minimum and maximum x across all points.
48        int min = Integer.MAX_VALUE;
49        int max = 0;
50
51        // For each x: minY maps x -> minimum y, maxY maps x -> maximum y.
52        Map<Integer, Integer> minY = new HashMap<>();
53        Map<Integer, Integer> maxY = new HashMap<>();
54
55        for (int[] c : coords) {
56            int x = c[0];
57            int y = c[1];
58
59            // Track the overall horizontal range.
60            min = Math.min(min, x);
61            max = Math.max(max, x);
62
63            // Update the vertical extent recorded for this x value.
64            if (minY.containsKey(x)) {
65                minY.put(x, Math.min(minY.get(x), y));
66                maxY.put(x, Math.max(maxY.get(x), y));
67            } else {
68                minY.put(x, y);
69                maxY.put(x, y);
70            }
71        }
72
73        long ans = 0;
74        // Iterate over every distinct x that has at least one point.
75        for (Map.Entry<Integer, Integer> entry : minY.entrySet()) {
76            int x = entry.getKey();
77            int y = entry.getValue();
78
79            // Vertical leg length: difference between max and min y at this x.
80            int d = maxY.get(x) - y;
81
82            // Horizontal leg length: farthest distance from x to a boundary.
83            ans = Math.max(ans, (long) d * Math.max(max - x, x - min));
84        }
85        return ans;
86    }
87}
88
1class Solution {
2public:
3    long long maxArea(vector<vector<int>>& coords) {
4        // Lambda that computes the maximum rectangle area for the current orientation.
5        // For a fixed x-coordinate, two points sharing that x define a vertical edge
6        // (height = max_y - min_y). The opposite edge can be placed at the farthest
7        // x-coordinate (either the global minimum or maximum x), giving the width.
8        auto calc = [&]() -> long long {
9            int minX = INT_MAX, maxX = 0;
10
11            // minYByX: minimum y observed for each x-coordinate.
12            // maxYByX: maximum y observed for each x-coordinate.
13            unordered_map<int, int> minYByX, maxYByX;
14
15            // First pass: collect global x-range and per-x y-range.
16            for (auto& point : coords) {
17                int x = point[0], y = point[1];
18                minX = min(minX, x);
19                maxX = max(maxX, x);
20                if (minYByX.count(x)) {
21                    minYByX[x] = min(minYByX[x], y);
22                    maxYByX[x] = max(maxYByX[x], y);
23                } else {
24                    minYByX[x] = y;
25                    maxYByX[x] = y;
26                }
27            }
28
29            long long best = 0;
30            // For each distinct x, the vertical edge height is the y-span at that x.
31            // The width is the larger horizontal distance to the global x bounds.
32            for (auto& [x, minY] : minYByX) {
33                int height = maxYByX[x] - minY;
34                int width = max(maxX - x, x - minX);
35                best = max(best, 1LL * height * width);
36            }
37            return best;
38        };
39
40        // Compute the answer with the original orientation.
41        long long ans = calc();
42
43        // Swap x and y of every point to evaluate the transposed orientation,
44        // which covers rectangles whose fixed edge is horizontal instead of vertical.
45        for (auto& point : coords) {
46            swap(point[0], point[1]);
47        }
48        ans = max(ans, calc());
49
50        // A valid rectangle requires a positive area; otherwise return -1.
51        return ans > 0 ? ans : -1;
52    }
53};
54
1/**
2 * Computes the maximum area of an axis-aligned right triangle that can be
3 * formed from the given points, where the right-angle legs are parallel to
4 * the axes. Returns -1 if no valid triangle exists.
5 *
6 * @param coords - Array of [x, y] points.
7 * @returns The maximum triangle area (doubled, since the 1/2 factor is omitted), or -1.
8 */
9function maxArea(coords: number[][]): number {
10    /**
11     * For the current orientation of the points, find the best triangle whose
12     * vertical leg lies on a shared x-coordinate. The two points sharing an x
13     * value provide the vertical side; the horizontal distance to the farthest
14     * x boundary provides the base.
15     */
16    function calc(): number {
17        // Track the global minimum and maximum x-coordinates.
18        let minX = Infinity;
19        let maxX = 0;
20
21        // For each x: minY stores the smallest y, maxY stores the largest y.
22        const minYByX = new Map<number, number>();
23        const maxYByX = new Map<number, number>();
24
25        for (const [x, y] of coords) {
26            minX = Math.min(minX, x);
27            maxX = Math.max(maxX, x);
28
29            if (minYByX.has(x)) {
30                // Update the existing y-range for this x-coordinate.
31                minYByX.set(x, Math.min(minYByX.get(x)!, y));
32                maxYByX.set(x, Math.max(maxYByX.get(x)!, y));
33            } else {
34                // First point seen at this x-coordinate.
35                minYByX.set(x, y);
36                maxYByX.set(x, y);
37            }
38        }
39
40        let best = 0;
41        for (const [x, low] of minYByX) {
42            // Height of the vertical leg at this x-coordinate.
43            const height = maxYByX.get(x)! - low;
44            // Base is the horizontal distance to the farthest boundary.
45            const base = Math.max(maxX - x, x - minX);
46            best = Math.max(best, height * base);
47        }
48        return best;
49    }
50
51    // First pass: vertical legs (shared x-coordinates).
52    let ans = calc();
53
54    // Swap x and y for every point, then run again to handle horizontal legs.
55    for (const point of coords) {
56        [point[0], point[1]] = [point[1], point[0]];
57    }
58    ans = Math.max(ans, calc());
59
60    // A valid triangle requires a positive area; otherwise return -1.
61    return ans > 0 ? ans : -1;
62}
63

Time and Space Complexity

  • Time Complexity: O(n), where n is the length of coords. The calc function iterates through all coordinates once to build the dictionaries f and g (taking O(n) time), then iterates over the entries of f once more (at most O(n) entries). The calc function is invoked twice, and the loop that swaps the coordinates also runs in O(n). Therefore, the overall time complexity is O(n).

  • Space Complexity: O(n). The dictionaries f and g store at most one entry per distinct x value, requiring O(n) space in the worst case. Hence, the space complexity is O(n).

Pattern Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Initializing max_x = 0 instead of -inf

The most dangerous bug in this code is the initialization of max_x:

min_x, max_x = inf, 0   # ❌ max_x = 0 is WRONG

Why it's a problem:

The problem states that points live in an infinite Cartesian plane, which means x-coordinates can be negative. If all x-coordinates are negative (e.g., coords = [[-5, 1], [-5, 10], [-2, 3]]), then max_x will incorrectly stay at 0, because no negative value can exceed the initial 0.

This corrupts the height calculation:

height = max(max_x - x, x - min_x)

With max_x = 0 artificially inflated, max_x - x produces a height larger than any real point can justify, yielding a wrong (too large) area — or in some edge cases, a phantom "valid" triangle where none exists.

Solution: Initialize max_x to negative infinity so it works for all integer ranges:

min_x, max_x = inf, -inf   # ✅ Correct for negative coordinates

Pitfall 2: Mutating the input coords in place

The swap step modifies the caller's data:

for point in coords:
    point[0], point[1] = point[1], point[0]

Why it's a problem:

  • After maxArea returns, the caller's coords array is left in a swapped state (since the swap is applied twice — once per calc() setup — wait, actually only once here, leaving it permanently transposed).
  • If the caller reuses coords afterward, or if the function is called twice (e.g., in repeated test harnesses), results become inconsistent.
  • This violates the principle of not having side effects on input arguments.

Solution: Either restore the coordinates after the second pass, or pass a transposed copy into calc. A clean fix is to make calc accept the iterable explicitly:

def calc(points) -> int:
    ...
    for x, y in points:
        ...

ans = calc(coords)
ans = max(ans, calc([(y, x) for x, y in coords]))   # transposed copy, no mutation

Pitfall 3: Treating ans == 0 ambiguously

The final check returns -1 whenever ans == 0:

return ans if ans else -1

Why it's subtle:

ans == 0 can arise in two distinct situations, and both happen to be correctly mapped to -1 here — but it's worth understanding why:

  1. No two points share an x or y coordinate, so no axis-parallel side exists → no valid triangle.
  2. A valid axis-parallel base exists, but the third point lies on the same line, giving height = 0 (collinear points → zero area).

In both cases base * height == 0, and a zero-area triangle is explicitly disallowed by the problem. So the single check correctly handles both. The pitfall is assuming ans == 0 only means "no parallel side"; in reality it also catches the collinear/degenerate case, which is exactly what the problem's note about "cannot have zero area" requires.

Solution: No code change is needed, but add a comment clarifying that ans == 0 covers both the "no valid base" and "degenerate triangle" cases:

# ans == 0 means either no axis-parallel side exists, or every candidate
# triangle is degenerate (third point collinear with the base). Both are invalid.
return ans if ans else -1

Summary of the corrected calc initialization

The single most impactful fix is the max_x initialization, since it produces silently incorrect answers on valid inputs:

min_x, max_x = inf, -inf   # handles negative coordinates correctly

Ready to land your dream job?

Unlock your dream job with a 5-minute quiz for a personalized study roadmap!

Get My Roadmap
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Get a Personalized Study Roadmap:

What's the output of running the following function using input 56?

1KEYBOARD = {
2    '2': 'abc',
3    '3': 'def',
4    '4': 'ghi',
5    '5': 'jkl',
6    '6': 'mno',
7    '7': 'pqrs',
8    '8': 'tuv',
9    '9': 'wxyz',
10}
11
12def letter_combinations_of_phone_number(digits):
13    def dfs(path, res):
14        if len(path) == len(digits):
15            res.append(''.join(path))
16            return
17
18        next_number = digits[len(path)]
19        for letter in KEYBOARD[next_number]:
20            path.append(letter)
21            dfs(path, res)
22            path.pop()
23
24    res = []
25    dfs([], res)
26    return res
27
1private static final Map<Character, char[]> KEYBOARD = Map.of(
2    '2', "abc".toCharArray(),
3    '3', "def".toCharArray(),
4    '4', "ghi".toCharArray(),
5    '5', "jkl".toCharArray(),
6    '6', "mno".toCharArray(),
7    '7', "pqrs".toCharArray(),
8    '8', "tuv".toCharArray(),
9    '9', "wxyz".toCharArray()
10);
11
12public static List<String> letterCombinationsOfPhoneNumber(String digits) {
13    List<String> res = new ArrayList<>();
14    dfs(new StringBuilder(), res, digits.toCharArray());
15    return res;
16}
17
18private static void dfs(StringBuilder path, List<String> res, char[] digits) {
19    if (path.length() == digits.length) {
20        res.add(path.toString());
21        return;
22    }
23    char next_digit = digits[path.length()];
24    for (char letter : KEYBOARD.get(next_digit)) {
25        path.append(letter);
26        dfs(path, res, digits);
27        path.deleteCharAt(path.length() - 1);
28    }
29}
30
1const KEYBOARD = {
2    '2': 'abc',
3    '3': 'def',
4    '4': 'ghi',
5    '5': 'jkl',
6    '6': 'mno',
7    '7': 'pqrs',
8    '8': 'tuv',
9    '9': 'wxyz',
10}
11
12function letter_combinations_of_phone_number(digits) {
13    let res = [];
14    dfs(digits, [], res);
15    return res;
16}
17
18function dfs(digits, path, res) {
19    if (path.length === digits.length) {
20        res.push(path.join(''));
21        return;
22    }
23    let next_number = digits.charAt(path.length);
24    for (let letter of KEYBOARD[next_number]) {
25        path.push(letter);
26        dfs(digits, path, res);
27        path.pop();
28    }
29}
30

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More