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 elementtrees[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
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:
- First sorting all points by x-coordinate (and y-coordinate as tiebreaker)
- Building the lower hull by going from left to right, removing points that would cause right turns
- Building the upper hull by going from right to left, again removing points that would cause right turns
- 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
to0
) - 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 EvaluatorExample 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:
-
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 takesO(n)
time. -
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 amortizedO(1)
per point, resulting inO(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:
-
Visited array
vis
: TakesO(n)
space to track which points are already in the convex hull. -
Stack
stk
: In the worst case (when all points are on the convex hull), the stack can contain alln
point indices, requiringO(n)
space. -
Output list: The final list comprehension creates a new list that can contain up to
n
points in the worst case, usingO(n)
space. -
Other variables: Variables like
i
,m
, etc., useO(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.
Depth first search is equivalent to which of the tree traversal order?
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!