Facebook Pixel

587. Erect the Fence

Problem Description

You have a garden with trees planted at various coordinates. Each tree's position is given as [x, y] coordinates in a 2D plane. Your task is to build a fence around all the trees using the minimum amount of rope possible.

The fence must enclose all trees in the garden - no tree can be left outside the fence. Since you want to use the minimum length of rope, the fence will form a convex hull around all the trees.

You need to return the coordinates of all trees that are located exactly on the fence perimeter. These are the trees that the fence rope will touch or pass through. Trees that are inside the fence but not on its boundary should not be included in the output.

For example, if you have trees forming a triangle with one tree in the middle, only the three trees forming the triangle would be on the fence perimeter, while the middle tree would be enclosed but not part of the answer.

The answer can be returned in any order.

Key Points:

  • Input: An array trees where each element trees[i] = [xi, yi] represents a tree's coordinates
  • Output: Coordinates of all trees that lie on the fence perimeter
  • The fence forms the smallest perimeter that encloses all trees (convex hull)
  • If there are fewer than 4 trees, all trees will be on the perimeter
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The problem is asking us to find the convex hull of a set of points. Think of it like wrapping a rubber band around all the trees - the rubber band will naturally form the smallest perimeter that encloses all points, and only certain trees will actually touch the rubber band.

To find which trees are on this boundary, we can use a technique called the "monotone chain algorithm" or "Andrew's algorithm". The key insight is that we can build the convex hull in two parts: the lower hull and the upper hull.

Imagine walking along the fence from the leftmost point to the rightmost point. As we walk, we should always be making either left turns or going straight - never right turns. If we ever need to make a right turn to include a new point, it means our previous point shouldn't be on the hull.

We can detect whether three points form a left turn or right turn using the cross product. For three points A, B, and C, the cross product (B - A) × (C - B) tells us the turn direction:

  • Positive value: left turn (counterclockwise)
  • Negative value: right turn (clockwise)
  • Zero: collinear (straight line)

The algorithm works by:

  1. First sorting all points by x-coordinate (and y-coordinate as tiebreaker)
  2. Building the lower hull by going from left to right, removing points that would cause right turns
  3. Building the upper hull by going from right to left, again removing points that would cause right turns
  4. The combination of lower and upper hulls gives us the complete convex hull

The stack-based approach allows us to efficiently backtrack when we discover that a previously added point shouldn't be on the hull. We keep popping points from the stack until adding the new point would no longer create a right turn.

Learn more about Math patterns.

Solution Approach

The implementation uses the Monotone Chain Algorithm (Andrew's Algorithm) to find the convex hull. Let's walk through the code step by step:

1. Cross Product Function:

def cross(i, j, k):
    a, b, c = trees[i], trees[j], trees[k]
    return (b[0] - a[0]) * (c[1] - b[1]) - (b[1] - a[1]) * (c[0] - b[0])

This calculates the cross product of vectors AB and BC where A = trees[i], B = trees[j], C = trees[k]. A negative value indicates a right turn (clockwise), which we want to avoid on the convex hull.

2. Base Case:

if n < 4:
    return trees

With fewer than 4 points, all points must be on the convex hull perimeter.

3. Sort the Points:

trees.sort()

Points are sorted lexicographically (first by x-coordinate, then by y-coordinate). This ensures we start from the leftmost point.

4. Build Lower Hull:

vis = [False] * n
stk = [0]
for i in range(1, n):
    while len(stk) > 1 and cross(stk[-2], stk[-1], i) < 0:
        vis[stk.pop()] = False
    vis[i] = True
    stk.append(i)
  • We use a stack stk to maintain the current hull points
  • The vis array tracks which points are currently on the hull
  • For each new point i, we check if adding it would create a right turn
  • If yes, we pop the previous point from the stack (it's not on the hull)
  • We continue popping until adding point i would create a left turn or straight line
  • Then we add point i to the stack

5. Build Upper Hull:

m = len(stk)
for i in range(n - 2, -1, -1):
    if vis[i]:
        continue
    while len(stk) > m and cross(stk[-2], stk[-1], i) < 0:
        stk.pop()
    stk.append(i)
  • m stores the size of the lower hull to prevent removing those points
  • We traverse points from right to left (indices n-2 to 0)
  • Skip points already in the lower hull (vis[i] == True)
  • Apply the same logic: remove points that would cause right turns
  • The condition len(stk) > m ensures we don't remove lower hull points

6. Final Cleanup:

stk.pop()
return [trees[i] for i in stk]
  • The last point is removed because it's the same as the first point (we've completed the circuit)
  • Convert stack indices to actual tree coordinates

The algorithm runs in O(n log n) time due to sorting, and the hull construction is O(n) since each point is added and removed at most once.

Ready to land your dream job?

Unlock your dream job with a 5-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example with 5 trees to illustrate how the algorithm finds the convex hull.

Input: trees = [[1,1], [2,2], [2,0], [2,4], [3,3]]

Step 1: Sort the points After sorting by x-coordinate (then y-coordinate): trees = [[1,1], [2,0], [2,2], [2,4], [3,3]] Indices: 0 1 2 3 4

Step 2: Build Lower Hull

  • Start with stk = [0] (leftmost point [1,1])
  • Add point 1 ([2,0]):
    • Only one point in stack, so add directly
    • stk = [0, 1]
  • Add point 2 ([2,2]):
    • Check turn: points [1,1], [2,0], [2,2]
    • Cross product = (1)×(2) - (-1)×(0) = 2 (positive = left turn ✓)
    • Add to stack: stk = [0, 1, 2]
  • Add point 3 ([2,4]):
    • Check turn: points [2,0], [2,2], [2,4]
    • Cross product = (0)×(2) - (2)×(0) = 0 (collinear ✓)
    • Add to stack: stk = [0, 1, 2, 3]
  • Add point 4 ([3,3]):
    • Check turn: points [2,2], [2,4], [3,3]
    • Cross product = (0)×(-1) - (2)×(1) = -2 (negative = right turn ✗)
    • Pop point 3, now check [2,0], [2,2], [3,3]
    • Cross product = (0)×(1) - (2)×(1) = -2 (right turn ✗)
    • Pop point 2, now check [1,1], [2,0], [3,3]
    • Cross product = (1)×(3) - (-1)×(1) = 4 (left turn ✓)
    • Add point 4: stk = [0, 1, 4]

Lower hull complete: stk = [0, 1, 4], vis = [T, T, F, F, T]

Step 3: Build Upper Hull

  • m = 3 (size of lower hull)
  • Process point 3 ([2,4]):
    • Not visited, check turn: [2,0], [3,3], [2,4]
    • Cross product = (1)×(-1) - (3)×(-1) = 2 (left turn ✓)
    • Add to stack: stk = [0, 1, 4, 3]
  • Process point 2 ([2,2]): Already visited, skip
  • Process point 1 ([2,0]): Already visited, skip
  • Process point 0 ([1,1]): Already visited, skip

Step 4: Remove duplicate and return

  • Pop last element (it's the starting point again)
  • stk = [0, 1, 4, 3]
  • Return coordinates: [[1,1], [2,0], [3,3], [2,4]]

Visualization:

4 |     [2,4]
3 |         [3,3]
2 |     [2,2]     <- Inside hull, not on perimeter
1 | [1,1]
0 |     [2,0]
  +---------------
    1   2   3

The fence connects [1,1] → [2,0] → [3,3] → [2,4] → back to [1,1], with [2,2] enclosed inside but not touching the fence.

Solution Implementation

1class Solution:
2    def outerTrees(self, trees: List[List[int]]) -> List[List[int]]:
3        """
4        Find the convex hull of a set of points (trees) using Graham's scan algorithm.
5        Returns all points on the perimeter of the convex hull.
6      
7        Args:
8            trees: List of [x, y] coordinates representing tree positions
9          
10        Returns:
11            List of points that form the convex hull
12        """
13      
14        def calculate_cross_product(index_a: int, index_b: int, index_c: int) -> int:
15            """
16            Calculate the cross product of vectors AB and BC.
17          
18            Positive value: C is to the left of line AB (counter-clockwise turn)
19            Negative value: C is to the right of line AB (clockwise turn)
20            Zero: A, B, C are collinear
21          
22            Args:
23                index_a: Index of point A in trees array
24                index_b: Index of point B in trees array
25                index_c: Index of point C in trees array
26              
27            Returns:
28                Cross product value
29            """
30            point_a, point_b, point_c = trees[index_a], trees[index_b], trees[index_c]
31            vector_ab_x = point_b[0] - point_a[0]
32            vector_ab_y = point_b[1] - point_a[1]
33            vector_bc_x = point_c[0] - point_b[0]
34            vector_bc_y = point_c[1] - point_b[1]
35          
36            return vector_ab_x * vector_bc_y - vector_ab_y * vector_bc_x
37      
38        num_trees = len(trees)
39      
40        # If there are 3 or fewer trees, they all form the convex hull
41        if num_trees < 4:
42            return trees
43      
44        # Sort trees by x-coordinate first, then by y-coordinate
45        trees.sort()
46      
47        # Track which points have been visited in the lower hull
48        is_visited = [False] * num_trees
49      
50        # Stack to store indices of points in the convex hull
51        hull_stack = [0]
52      
53        # Build lower hull (left to right)
54        for current_index in range(1, num_trees):
55            # Remove points that create a clockwise turn (not part of convex hull)
56            while len(hull_stack) > 1 and calculate_cross_product(
57                hull_stack[-2], hull_stack[-1], current_index
58            ) < 0:
59                removed_index = hull_stack.pop()
60                is_visited[removed_index] = False
61          
62            is_visited[current_index] = True
63            hull_stack.append(current_index)
64      
65        # Mark the size of lower hull to know when to stop popping during upper hull construction
66        lower_hull_size = len(hull_stack)
67      
68        # Build upper hull (right to left)
69        for current_index in range(num_trees - 2, -1, -1):
70            # Skip points already in the lower hull
71            if is_visited[current_index]:
72                continue
73          
74            # Remove points that create a clockwise turn
75            while len(hull_stack) > lower_hull_size and calculate_cross_product(
76                hull_stack[-2], hull_stack[-1], current_index
77            ) < 0:
78                hull_stack.pop()
79          
80            hull_stack.append(current_index)
81      
82        # Remove the last point as it's the same as the first point (closes the hull)
83        hull_stack.pop()
84      
85        # Convert indices back to coordinates
86        return [trees[index] for index in hull_stack]
87
1class Solution {
2    /**
3     * Finds the convex hull of given points using Andrew's Monotone Chain algorithm.
4     * The convex hull is the smallest convex polygon that contains all points.
5     * 
6     * @param trees 2D array where each element represents a point [x, y]
7     * @return Array of points that form the convex hull
8     */
9    public int[][] outerTrees(int[][] trees) {
10        int n = trees.length;
11      
12        // If less than 4 points, all points are on the convex hull
13        if (n < 4) {
14            return trees;
15        }
16      
17        // Sort points lexicographically (first by x-coordinate, then by y-coordinate)
18        Arrays.sort(trees, (point1, point2) -> {
19            return point1[0] == point2[0] ? point1[1] - point2[1] : point1[0] - point2[0];
20        });
21      
22        // Track which points are already included in the hull
23        boolean[] visited = new boolean[n];
24      
25        // Stack to store indices of points in the convex hull
26        int[] stack = new int[n + 10];
27        int stackSize = 1;  // Start with the first point (index 0)
28      
29        // Build lower hull (left to right)
30        for (int i = 1; i < n; ++i) {
31            // Remove points that make a clockwise turn (negative cross product)
32            while (stackSize > 1 && 
33                   crossProduct(trees[stack[stackSize - 1]], 
34                               trees[stack[stackSize - 2]], 
35                               trees[i]) < 0) {
36                visited[stack[--stackSize]] = false;
37            }
38            visited[i] = true;
39            stack[stackSize++] = i;
40        }
41      
42        // Mark the size of lower hull
43        int lowerHullSize = stackSize;
44      
45        // Build upper hull (right to left)
46        for (int i = n - 1; i >= 0; --i) {
47            // Skip points already in lower hull
48            if (visited[i]) {
49                continue;
50            }
51          
52            // Remove points that make a clockwise turn (negative cross product)
53            while (stackSize > lowerHullSize && 
54                   crossProduct(trees[stack[stackSize - 1]], 
55                               trees[stack[stackSize - 2]], 
56                               trees[i]) < 0) {
57                --stackSize;
58            }
59            stack[stackSize++] = i;
60        }
61      
62        // Convert stack indices to actual points
63        // Exclude last point as it's duplicate of the first point
64        int[][] result = new int[stackSize - 1][2];
65        for (int i = 0; i < result.length; ++i) {
66            result[i] = trees[stack[i]];
67        }
68      
69        return result;
70    }
71  
72    /**
73     * Calculates the cross product of vectors BA and BC.
74     * Positive value indicates counter-clockwise turn.
75     * Negative value indicates clockwise turn.
76     * Zero indicates collinear points.
77     * 
78     * @param a Point A
79     * @param b Point B
80     * @param c Point C
81     * @return Cross product value
82     */
83    private int crossProduct(int[] a, int[] b, int[] c) {
84        return (b[0] - a[0]) * (c[1] - b[1]) - (b[1] - a[1]) * (c[0] - b[0]);
85    }
86}
87
1class Solution {
2public:
3    vector<vector<int>> outerTrees(vector<vector<int>>& trees) {
4        int n = trees.size();
5      
6        // If less than 4 points, all points are on the convex hull
7        if (n < 4) {
8            return trees;
9        }
10      
11        // Sort points lexicographically (first by x, then by y)
12        sort(trees.begin(), trees.end());
13      
14        // Track which points have been added to the hull
15        vector<bool> used(n, false);
16      
17        // Stack to store indices of points on the convex hull
18        vector<int> hull(n + 10);
19        int hullSize = 1;  // Start with the leftmost point (index 0)
20      
21        // Build lower hull (left to right)
22        for (int i = 1; i < n; ++i) {
23            // Remove points that create a right turn (negative cross product)
24            // We want to keep only left turns or collinear points
25            while (hullSize > 1 && 
26                   crossProduct(trees[hull[hullSize - 2]], 
27                               trees[hull[hullSize - 1]], 
28                               trees[i]) < 0) {
29                used[hull[--hullSize]] = false;
30            }
31            used[i] = true;
32            hull[hullSize++] = i;
33        }
34      
35        // Mark the size of lower hull
36        int lowerHullSize = hullSize;
37      
38        // Build upper hull (right to left)
39        for (int i = n - 1; i >= 0; --i) {
40            // Skip points already in lower hull
41            if (used[i]) {
42                continue;
43            }
44          
45            // Remove points that create a right turn when building upper hull
46            while (hullSize > lowerHullSize && 
47                   crossProduct(trees[hull[hullSize - 2]], 
48                               trees[hull[hullSize - 1]], 
49                               trees[i]) < 0) {
50                --hullSize;
51            }
52            hull[hullSize++] = i;
53        }
54      
55        // Construct result from hull indices
56        // Last point is duplicate of first point, so exclude it
57        vector<vector<int>> result;
58        for (int i = 0; i < hullSize - 1; ++i) {
59            result.push_back(trees[hull[i]]);
60        }
61      
62        return result;
63    }
64
65private:
66    // Calculate cross product of vectors AB and BC
67    // Positive: left turn, Negative: right turn, Zero: collinear
68    int crossProduct(vector<int>& pointA, vector<int>& pointB, vector<int>& pointC) {
69        int vectorAB_x = pointB[0] - pointA[0];
70        int vectorAB_y = pointB[1] - pointA[1];
71        int vectorBC_x = pointC[0] - pointB[0];
72        int vectorBC_y = pointC[1] - pointB[1];
73      
74        return vectorAB_x * vectorBC_y - vectorAB_y * vectorBC_x;
75    }
76};
77
1/**
2 * Finds all points on the convex hull of a set of trees (points)
3 * Uses Andrew's monotone chain algorithm to build the convex hull
4 * @param trees - Array of points represented as [x, y] coordinates
5 * @returns Array of points that form the convex hull
6 */
7function outerTrees(trees: number[][]): number[][] {
8    const n = trees.length;
9  
10    // If less than 4 points, all points are on the convex hull
11    if (n < 4) {
12        return trees;
13    }
14  
15    // Sort points lexicographically (first by x, then by y)
16    trees.sort((a, b) => {
17        if (a[0] !== b[0]) return a[0] - b[0];
18        return a[1] - b[1];
19    });
20  
21    // Track which points have been added to the hull
22    const used: boolean[] = new Array(n).fill(false);
23  
24    // Array to store indices of points on the convex hull
25    const hull: number[] = new Array(n + 10);
26    let hullSize = 1;  // Start with the leftmost point (index 0)
27    hull[0] = 0;
28  
29    // Build lower hull (left to right)
30    for (let i = 1; i < n; i++) {
31        // Remove points that create a right turn (negative cross product)
32        // We want to keep only left turns or collinear points
33        while (hullSize > 1 && 
34               calculateCrossProduct(trees[hull[hullSize - 2]], 
35                                   trees[hull[hullSize - 1]], 
36                                   trees[i]) < 0) {
37            hullSize--;
38            used[hull[hullSize]] = false;
39        }
40        used[i] = true;
41        hull[hullSize] = i;
42        hullSize++;
43    }
44  
45    // Mark the size of lower hull
46    const lowerHullSize = hullSize;
47  
48    // Build upper hull (right to left)
49    for (let i = n - 1; i >= 0; i--) {
50        // Skip points already in lower hull
51        if (used[i]) {
52            continue;
53        }
54      
55        // Remove points that create a right turn when building upper hull
56        while (hullSize > lowerHullSize && 
57               calculateCrossProduct(trees[hull[hullSize - 2]], 
58                                   trees[hull[hullSize - 1]], 
59                                   trees[i]) < 0) {
60            hullSize--;
61        }
62        hull[hullSize] = i;
63        hullSize++;
64    }
65  
66    // Construct result from hull indices
67    // Last point is duplicate of first point, so exclude it
68    const result: number[][] = [];
69    for (let i = 0; i < hullSize - 1; i++) {
70        result.push(trees[hull[i]]);
71    }
72  
73    return result;
74}
75
76/**
77 * Calculate cross product of vectors AB and BC
78 * @param pointA - First point
79 * @param pointB - Second point (shared between both vectors)
80 * @param pointC - Third point
81 * @returns Cross product value:
82 *          Positive: left turn
83 *          Negative: right turn
84 *          Zero: collinear points
85 */
86function calculateCrossProduct(pointA: number[], pointB: number[], pointC: number[]): number {
87    // Calculate vector AB
88    const vectorAB_x = pointB[0] - pointA[0];
89    const vectorAB_y = pointB[1] - pointA[1];
90  
91    // Calculate vector BC
92    const vectorBC_x = pointC[0] - pointB[0];
93    const vectorBC_y = pointC[1] - pointB[1];
94  
95    // Return cross product: AB × BC
96    return vectorAB_x * vectorBC_y - vectorAB_y * vectorBC_x;
97}
98

Time and Space Complexity

Time Complexity: O(n log n)

The time complexity is dominated by the sorting operation trees.sort() which takes O(n log n) time where n is the number of points. After sorting, the algorithm performs two passes through the points:

  1. First pass (building upper hull): Iterates through all n points once. For each point, it may pop elements from the stack. Since each element can be pushed and popped at most once, this pass takes O(n) time.

  2. Second pass (building lower hull): Similarly iterates through all n points in reverse order. Each point is processed at most once, and stack operations are amortized O(1) per point, resulting in O(n) time.

The cross function performs constant time operations O(1) for each call.

Overall: O(n log n) + O(n) + O(n) = O(n log n)

Space Complexity: O(n)

The space complexity consists of:

  1. Visited array vis: Takes O(n) space to track which points are already in the convex hull.

  2. Stack stk: In the worst case (when all points are on the convex hull), the stack can contain all n point indices, requiring O(n) space.

  3. Output list: The final list comprehension creates a new list that can contain up to n points in the worst case, using O(n) space.

  4. Other variables: Variables like i, m, etc., use O(1) space.

Total space complexity: O(n) + O(n) + O(n) = O(n)

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

Common Pitfalls

1. Not Handling Collinear Points on the Hull Edges

The most significant pitfall in convex hull algorithms is incorrectly handling collinear points (points that lie on the same straight line). When multiple trees are aligned on what should be an edge of the fence, the standard convex hull algorithm might exclude some of these intermediate points.

Example Problem: If you have trees at coordinates [[0,0], [0,2], [0,4], [2,2]], the trees at [0,0], [0,2], and [0,4] are collinear. A basic convex hull might only include [0,0] and [0,4] as vertices, missing [0,2] which also lies on the fence perimeter.

Why This Happens: The cross product returns 0 for collinear points, and the condition cross(...) < 0 only removes points with negative cross products (right turns). Collinear points aren't explicitly handled, leading to their potential exclusion.

Solution: Modify the algorithm to include all collinear points on the hull edges:

def outerTrees(self, trees: List[List[int]]) -> List[List[int]]:
    def cross(i, j, k):
        a, b, c = trees[i], trees[j], trees[k]
        return (b[0] - a[0]) * (c[1] - b[1]) - (b[1] - a[1]) * (c[0] - b[0])
  
    n = len(trees)
    if n < 4:
        return trees
  
    trees.sort()
  
    # Build lower hull
    lower = []
    for i in range(n):
        while len(lower) > 1 and cross(lower[-2], lower[-1], i) < 0:
            lower.pop()
        lower.append(i)
  
    # Build upper hull
    upper = []
    for i in range(n - 1, -1, -1):
        while len(upper) > 1 and cross(upper[-2], upper[-1], i) < 0:
            upper.pop()
        upper.append(i)
  
    # Remove duplicates and combine
    hull_indices = set(lower + upper)
    return [trees[i] for i in hull_indices]

2. Incorrect Handling of Duplicate Points

Problem: If the input contains duplicate tree coordinates, the algorithm might include the same point multiple times in the result or fail to handle them properly.

Solution: Either deduplicate the input first or use a set to track unique points:

# Option 1: Deduplicate input
unique_trees = []
seen = set()
for tree in trees:
    tree_tuple = tuple(tree)
    if tree_tuple not in seen:
        seen.add(tree_tuple)
        unique_trees.append(tree)
trees = unique_trees

# Option 2: Use set for final result
hull_points = list({tuple(trees[i]) for i in hull_stack})
return [list(point) for point in hull_points]

3. Stack Index Management Issues

Problem: The relationship between the vis array and the stack can become inconsistent, especially when popping elements. The current code sets vis[removed_index] = False when popping from the lower hull, but doesn't track visibility during upper hull construction.

Solution: Either eliminate the vis array entirely (as shown in the collinear solution) or maintain it consistently throughout both hull constructions. The cleaner approach is to build separate lower and upper hulls and combine them using a set to handle duplicates automatically.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Depth first search is equivalent to which of the tree traversal order?


Recommended Readings

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

Load More