Erect the Fence
HardPrompt
You are given an array trees where trees[i] = [xi, yi] represents the location of a tree in the garden.
Fence the entire garden using the minimum length of rope, as it is expensive. The garden is well-fenced only if all the trees are enclosed.
Return the coordinates of trees that are exactly located on the fence perimeter. You may return the answer in any order.
Example 1:
Input: trees = [[1,1],[2,2],[2,0],[2,4],[3,3],[4,2]]
Output: [[1,1],[2,0],[4,2],[3,3],[2,4]]
Explanation: All the trees will be on the perimeter of the fence except the tree at [2, 2], which will be inside the fence.Example 2:
Input: trees = [[1,2],[2,2],[4,2]]
Output: [[4,2],[2,2],[1,2]]
Explanation: The fence forms a line that passes through all the trees.
Constraints:
1 <= trees.length <= 3000trees[i].length == 20 <= xi, yi <= 100- All the given positions are unique.
Approaches
2 approaches with complexity analysis and trade-offs.
This approach, also known as the Gift Wrapping algorithm, simulates wrapping a string around the set of points. It starts from an extreme point (e.g., the leftmost one) and iteratively finds the next point on the hull by selecting the one that creates the most "counter-clockwise" turn. This process continues until the hull wraps back to the starting point.
Algorithm
- Find the point with the smallest x-coordinate (leftmost point). If there's a tie, pick the one with the smallest y-coordinate. This point is guaranteed to be on the convex hull and will be our starting point.
- Start with the leftmost point as the current point
p. - Repeatedly find the next point
qon the hull. The pointqis chosen such that for any other pointr, the triplet(p, q, r)forms a counter-clockwise turn or is collinear. This meansqis the most counter-clockwise point with respect top. - To handle collinear points, if multiple points are equally counter-clockwise (i.e., they are collinear with
p), the one farthest frompis chosen as the next vertexq. - After finding
q, all points that lie on the line segmentpqare also part of the hull and must be added. - Add the new point(s) to the hull and set
p = q. - Repeat the process until the hull is closed, i.e., when the next point to be added is the starting point.
Walkthrough
The Jarvis's March algorithm is one of the simplest ways to compute the convex hull. It builds the hull one point at a time.
Here's a step-by-step breakdown:
- Find a starting point: We must begin with a point that is guaranteed to be on the hull. The point with the minimum x-coordinate (the leftmost point) is a perfect candidate. In case of a tie, the one with the minimum y-coordinate is chosen.
- Iterative Wrapping: From the current hull point
p, we search for the next hull pointq. We iterate through all other pointsrand find the one that has the smallest polar angle with respect top. This is equivalent to finding the pointrthat makes the most counter-clockwise turn from the vector extending fromp. The orientation can be determined using the cross product of vectors(p, q)and(p, r). - Handling Collinearity: A crucial part of the algorithm is handling points that are collinear. When we find the next point
q, we must also include any points that lie on the line segment between the current pointpandq. These points are part of the fence. - Termination: We add the newly found point(s) to our hull and repeat the process from the new point. The algorithm terminates when we wrap around and select our original starting point as the next point.
class Solution { // Returns a value indicating the orientation of the triplet (p, q, r) // > 0: Counter-clockwise (left turn) // < 0: Clockwise (right turn) // = 0: Collinear private int orientation(int[] p, int[] q, int[] r) { return (q[1] - p[1]) * (r[0] - q[0]) - (q[0] - p[0]) * (r[1] - q[1]); } // Returns the squared Euclidean distance between two points private int distanceSq(int[] p, int[] q) { return (p[0] - q[0]) * (p[0] - q[0]) + (p[1] - q[1]) * (p[1] - q[1]); } public int[][] outerTrees(int[][] trees) { if (trees.length <= 3) { return trees; } Set<int[]> hull = new HashSet<>(); // Find the leftmost point int startIdx = 0; for (int i = 1; i < trees.length; i++) { if (trees[i][0] < trees[startIdx][0]) { startIdx = i; } } int currentIdx = startIdx; do { int nextIdx = (currentIdx + 1) % trees.length; // Find the next point on the hull for (int i = 0; i < trees.length; i++) { int orient = orientation(trees[currentIdx], trees[nextIdx], trees[i]); if (orient > 0) { // trees[i] is more counter-clockwise nextIdx = i; } else if (orient == 0) { // Collinear case // If trees[i] is farther from currentIdx, it's a better candidate if (distanceSq(trees[currentIdx], trees[i]) > distanceSq(trees[currentIdx], trees[nextIdx])) { nextIdx = i; } } } // Add all points on the segment from currentIdx to nextIdx for (int i = 0; i < trees.length; i++) { if (orientation(trees[currentIdx], trees[nextIdx], trees[i]) == 0) { hull.add(trees[i]); } } currentIdx = nextIdx; } while (currentIdx != startIdx); return hull.toArray(new int[hull.size()][]); }}Complexity
Time
O(N * H), where N is the total number of trees and H is the number of trees on the fence. For each of the H hull points, we iterate through all N points to find the next one. In the worst case, H can be equal to N, leading to a time complexity of O(N^2).
Space
O(N) to store the hull points. In the worst case, all N points are on the hull.
Trade-offs
Pros
Conceptually simple and easy to understand the high-level idea.
It's an output-sensitive algorithm, meaning its performance depends on the number of hull points (H). It's efficient if H is small.
Cons
The worst-case time complexity of O(N^2) makes it unsuitable for large datasets.
The implementation can be tricky, especially the logic to correctly handle collinear points to ensure all boundary points are included without errors.
This is a more efficient algorithm that builds the convex hull by constructing the upper and lower hulls separately. It first sorts all points by their x-coordinates (and y-coordinates for ties). Then, it iterates through the sorted points to build the lower hull, and then iterates in reverse to build the upper hull. This method is generally preferred for its efficiency and simpler implementation regarding collinear points.
Algorithm
- If there are 3 or fewer points, they all form the hull. Return them.
- Sort the input
treesarray lexicographically: first by x-coordinate, and then by y-coordinate for ties. - Initialize an empty list,
hull, to store the vertices of the hull. - Build the Lower Hull: Iterate through the sorted points from left to right. For each point
p, check if adding it to thehullwould create a clockwise turn. A clockwise turn indicates that the last point added to thehullis not part of the lower hull and should be removed. We repeatedly pop from thehullas long as a clockwise turn is formed. Then, add the current pointp. - Build the Upper Hull: Iterate through the sorted points from right to left. Apply the same logic as for the lower hull: pop from the
hullif a clockwise turn is formed, then add the current point. - The first and last points of the sorted array are processed twice (once for each hull), but the final result should not contain duplicates. Using a
Setto store the combined hull points handles this automatically. - Convert the set of hull points back to an array and return it.
Walkthrough
The Monotone Chain algorithm, also known as Andrew's algorithm, is an efficient method for finding the convex hull of a set of points. Its main advantage is that it avoids explicit angle calculations and relies on the cross product to check orientation, which is less prone to floating-point errors and is computationally faster.
The algorithm works as follows:
- Sort: The first step is to sort all the points based on their x-coordinates. If two points have the same x-coordinate, they are sorted by their y-coordinate. This gives us an ordered sequence of points from left to right.
- Build Hulls: The algorithm then constructs the convex hull in two parts: the lower hull and the upper hull.
- Lower Hull: We iterate through the sorted points and build the lower hull. We use a list or stack to maintain the current lower hull vertices. For each point, we check if adding it maintains a counter-clockwise chain. If adding the new point creates a clockwise turn, it means the previous point is concave and not part of the hull, so we pop it. We continue this until a counter-clockwise turn is restored. Collinear points are kept, as the condition for popping is a strictly clockwise turn.
- Upper Hull: We do a similar pass, but this time from right to left (iterating through the sorted points in reverse), to construct the upper hull.
- Combine: The final convex hull is the union of the lower and upper hulls. The start and end points of the sorted list are part of both hulls, so we need to remove duplicates when combining them. A
HashSetis an easy way to achieve this.
import java.util.ArrayList;import java.util.Arrays;import java.util.HashSet;import java.util.List;import java.util.Set; class Solution { // Helper function to calculate the cross product (orientation) // > 0 for counter-clockwise turn (left) // < 0 for clockwise turn (right) // = 0 for collinear points private int crossProduct(int[] p1, int[] p2, int[] p3) { return (p2[0] - p1[0]) * (p3[1] - p1[1]) - (p2[1] - p1[1]) * (p3[0] - p1[0]); } public int[][] outerTrees(int[][] trees) { if (trees.length <= 3) { return trees; } // Sort points lexicographically Arrays.sort(trees, (p1, p2) -> { if (p1[0] != p2[0]) { return p1[0] - p2[0]; } return p1[1] - p2[1]; }); List<int[]> lowerHull = new ArrayList<>(); // Build lower hull for (int[] point : trees) { while (lowerHull.size() >= 2 && crossProduct(lowerHull.get(lowerHull.size() - 2), lowerHull.get(lowerHull.size() - 1), point) < 0) { lowerHull.remove(lowerHull.size() - 1); } lowerHull.add(point); } List<int[]> upperHull = new ArrayList<>(); // Build upper hull for (int i = trees.length - 1; i >= 0; i--) { int[] point = trees[i]; while (upperHull.size() >= 2 && crossProduct(upperHull.get(upperHull.size() - 2), upperHull.get(upperHull.size() - 1), point) < 0) { upperHull.remove(upperHull.size() - 1); } upperHull.add(point); } // Combine hulls and remove duplicates Set<int[]> hull = new HashSet<>(lowerHull); hull.addAll(upperHull); return hull.toArray(new int[hull.size()][]); }}Complexity
Time
O(N log N), which is dominated by the initial sorting of the points. The two passes to build the upper and lower hulls each take O(N) time because each point is pushed onto and popped from the list at most once.
Space
O(N) to store the hull points. The lists for the lower and upper hulls can grow up to size N.
Trade-offs
Pros
Efficient with a reliable O(N log N) time complexity.
Robust and generally easier to implement correctly than other hull algorithms like Graham Scan, especially when handling collinear points.
Avoids trigonometric functions and floating-point arithmetic, making it fast and precise.
Cons
The O(N log N) complexity is dominated by sorting, which might be suboptimal if the points have some pre-existing order or special distribution where a linear-time hull algorithm could apply (though this is rare).
Solutions
Solution
class Solution {public int[][] outerTrees(int[][] trees) { int n = trees.length; if (n < 4) { return trees; } Arrays.sort( trees, (a, b)->{ return a[0] == b[0] ? a[1] - b[1] : a[0] - b[0]; }); boolean[] vis = new boolean[n]; int[] stk = new int[n + 10]; int cnt = 1; for (int i = 1; i < n; ++i) { while (cnt > 1 && cross(trees[stk[cnt - 1]], trees[stk[cnt - 2]], trees[i]) < 0) { vis[stk[--cnt]] = false; } vis[i] = true; stk[cnt++] = i; } int m = cnt; for (int i = n - 1; i >= 0; --i) { if (vis[i]) { continue; } while (cnt > m && cross(trees[stk[cnt - 1]], trees[stk[cnt - 2]], trees[i]) < 0) { --cnt; } stk[cnt++] = i; } int[][] ans = new int[cnt - 1][2]; for (int i = 0; i < ans.length; ++i) { ans[i] = trees[stk[i]]; } return ans; }private int cross(int[] a, int[] b, int[] c) { return (b[0] - a[0]) * (c[1] - b[1]) - (b[1] - a[1]) * (c[0] - b[0]); }}class Solution {public: vector<vector<int>> outerTrees(vector<vector<int>> &trees) { int n = trees.size(); if (n < 4) return trees; sort(trees.begin(), trees.end()); vector<int> vis(n); vector<int> stk(n + 10); int cnt = 1; for (int i = 1; i < n; ++i) { while (cnt > 1 && cross(trees[stk[cnt - 1]], trees[stk[cnt - 2]], trees[i]) < 0) vis[stk[--cnt]] = false; vis[i] = true; stk[cnt++] = i; } int m = cnt; for (int i = n - 1; i >= 0; --i) { if (vis[i]) continue; while (cnt > m && cross(trees[stk[cnt - 1]], trees[stk[cnt - 2]], trees[i]) < 0) --cnt; stk[cnt++] = i; } vector<vector<int>> ans; for (int i = 0; i < cnt - 1; ++i) ans.push_back(trees[stk[i]]); return ans; } int cross(vector<int> &a, vector<int> &b, vector<int> &c) { return (b[0] - a[0]) * (c[1] - b[1]) - (b[1] - a[1]) * (c[0] - b[0]); }};class Solution: 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() 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) 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) stk . pop() return [trees[i] for i in stk]Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.