Pebellslearn by building
0%
RoadmapsMatrixCoursesSandboxSign in
Computational Geometry  ›  Lesson

Convex Hull

Convex Hull 32 minAndrew's monotone chain builds the smallest convex boundary around a set of points by sorting once and then walking a stack: a new point pops any point behind it that would make a non-left turn
You're building a piece ofMeridian — service areas & coverage
This piece — convex_hull(): Wraps a depot's delivery stops in the smallest convex boundary that contains all of them.
Scenario A depot has its delivery stops plotted on the map, and Meridian needs the smallest boundary that contains every one of them — the depot's service area, drawn as a polygon instead of a vague radius.
Your task
Build convex_hull(points). Return the points on the convex hull of points, in counter-clockwise order, with no point repeated at the end. Deduplicate and sort points by (x, y) first. Build the LOWER hull by scanning left to right: for each new point, while the last two hull points and the new point do NOT make a left turn (orientation <= 0), pop the last hull point, then append the new point. Build the UPPER hull the same way scanning right to left. Join lower and upper, dropping the last point of each (it's the first point of the other half). Return [] for 0 or 1 points, or the points themselves for 2. Example: convex_hull([(0,0),(4,0),(4,4),(0,4),(2,2)]) -> [(0,0),(4,0),(4,4),(0,4)] (the interior point (2,2) is dropped).

Convex Hull

In short: the convex hull of a set of points is the smallest convex polygon that contains every one of them — stretch a rubber band around the points and let go, and it snaps to the hull. Andrew's monotone chain builds it in O(n log n): sort the points once by (x, y), then build two chains — a lower hull scanning left to right and an upper hull scanning right to left. Each chain is a stack: a new point is pushed on, but first, while the top of the stack and the new point do NOT make a left turn, the top point is popped — it would create a dent, so it can't be on the hull. Every point gets pushed once and popped at most once, so the whole walk after sorting is O(n).

A zigzag path of five points sorted left to right, with a dashed X marking the one point that gets popped because the point after it would need a right turn to reach, and the remaining four points connected by a solid line labelled "lower hull"

Wrapping a boundary around five stops

Five of Meridian's stops sit at (0,0), (1,-1), (2,0), (3,-1) and (4,0) — already sorted by x. Building the lower hull:

watch it move · stack

A new point is pushed, but first the stack pops any point that would make a non-left turn with it — a point that would dent the boundary inward never belongs on the hull.

Nothing to compare yet, so (0,0) just goes on. Stack, 1 item. Top is (0,0).

1 / 9
step through it
1def orientation(a, b, c):
2 cross = (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
3 if cross > 0:
4 return 1
5 if cross < 0:
6 return -1
7 return 0
8 
9def half_hull(points):
10 hull = []
11 for p in points:
12 while len(hull) >= 2 and orientation(hull[-2], hull[-1], p) <= 0:
13 hull.pop()
14 hull.append(p)
15 return hull
16 
17points = [(0, 0), (1, -1), (2, 0), (3, -1), (4, 0)]
18print(half_hull(points))

(2, 0) goes on the stack and comes right back off two points later — pushed once, popped once, never visited again. That's why the whole scan, after the one O(n log n) sort, costs O(n): the total number of pops across the entire run can never exceed the total number of pushes.

One hull, two halves

The lower hull alone only bounds the bottom of the point set. Run the exact same pop-on-non-left-turn rule on the points in reverse, and you trace the top:

python

The square's four corners come back; (2,2), (1,3) and (3,1) are all inside the square, so none of them survives either half's pops. Dropping lower[-1] and upper[-1] matters because each half's LAST point is the other half's FIRST point — joined without trimming, the hull would list two of its four corners twice.

sorted(set(points)) works above because stops is written as real Python tuples, and tuples are hashable. A point arriving from outside your code — from a test, a form, a file — usually arrives as an [x, y] list instead, and set() rejects a list outright: lists are mutable, so Python won't let them sit inside a set or be a dict key. Convert each point to a tuple first — sorted({tuple(p) for p in points}) — and it works regardless of which one arrives.

Quick practice

What does this print?
def orientation(a, b, c):
    cross = (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
    if cross > 0:
        return 1
    if cross < 0:
        return -1
    return 0

hull = [(0, 0), (1, -1)]
p = (2, 0)
print(len(hull) >= 2 and orientation(hull[-2], hull[-1], p) <= 0)
What does this print?
def orientation(a, b, c):
    cross = (b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0])
    if cross > 0:
        return 1
    if cross < 0:
        return -1
    return 0

def half_hull(points):
    hull = []
    for p in points:
        while len(hull) >= 2 and orientation(hull[-2], hull[-1], p) <= 0:
            hull.pop()
        hull.append(p)
    return hull

print(half_hull([(0, 0), (1, 1), (2, 2), (3, 0)]))

🎯 Your turn

Build convex_hull(points): return the convex hull's vertices, counter-clockwise, with duplicates removed and no point repeated at the end.

  • convex_hull([(0,0), (4,0), (4,4), (0,4), (2,2)]) → [(0,0), (4,0), (4,4), (0,4)]
  • convex_hull([(0,0), (1,0), (2,0), (3,0)]) → [(0,0), (3,0)]

Hint: points arrives as plain [x, y] lists, and a list can't go inside a set() — it isn't hashable. Dedupe with pts = sorted({tuple(p) for p in points}) instead. Build lower scanning pts left to right and upper scanning reversed(pts), popping on orientation(...) <= 0 each time. Return lower[:-1] + upper[:-1] (for 3+ points), converting each point back to a list before returning.

Press ▶ Run, try the Live App chips, then ✓ Check.

Practice — level up

0/5 solved

Solve each in its own editor — Run to try, Check to grade. Stuck? Reveal a Hint, or the full Solution + walkthrough.

Warm-upDedupe and sort the stops

Write dedupe_sort(points), returning the distinct points sorted by x, then by y for ties — the order the monotone chain needs to start from. points' elements arrive as plain [x, y] lists, not tuples. dedupe_sort([(4,0), (0,0), (4,0), (0,4)]) -> [[0,0], [0,4], [4,0]].

sortingconvex-hull
DrillBuild the lower hull

Write lower_hull(points), where points is already sorted by (x, y). Scan left to right, keeping a stack: before appending a new point, pop the stack's last point while the last two stack points and the new point do NOT make a left turn (orientation(...) <= 0). Return the stack. lower_hull([(0,0), (1,-1), (2,0), (3,-1), (4,0)]) -> [(0,0), (1,-1), (3,-1), (4,0)].

convex-hullmonotone-chain
BuildBuild the upper hull

Write upper_hull(points), where points is sorted by (x, y). Build it exactly like lower_hull, but scanning the points in REVERSE (right to left). Return the stack. upper_hull([(0,0), (1,-1), (2,0), (3,-1), (4,0)]) -> [(4,0), (0,0)].

convex-hullmonotone-chain
BossHow far around is the service boundary?

Write hull_perimeter(points): build the convex hull of points, then return its total perimeter (the sum of every edge's length, including the edge from the last hull point back to the first), rounded to 2 decimal places. hull_perimeter([(0,0), (4,0), (4,4), (0,4), (2,2)]) -> 16.0.

convex-hull
CapstoneIs this stop inside the service area?

Write point_in_convex_hull(hull, point), where hull is a convex hull's vertices in order (as convex_hull returns). Return True if point lies inside the hull or exactly on its boundary. Check every edge's orientation to point; it's inside only if every non-zero orientation is the SAME sign. point_in_convex_hull([(0,0),(4,0),(4,4),(0,4)], (2,2)) -> True.

convex-hullpoint-in-polygon
convex_hull.py
Call the function you wrote — just like your app's frontend would. Edit the inputs and hit Call.
convex_hull(points) → listThe convex hull's vertices, counter-clockwise, starting from the lexicographically smallest point.
try:
returned
Press Run to execute your code.
Press Check to run the tests for this lesson.