Convex Hull

The smallest convex polygon containing all the given points.

The algorithms

AlgorithmTimeNotes
Andrew’s monotone chainsort by ; the default — simplest, no angles, no precision headaches
Graham scansort by angle; more edge cases
Jarvis march (gift wrapping)output-sensitive, simple, slow
QuickHull avg, worstdivide and conquer
Chanoptimal output-sensitive, implementable
Kirkpatrick-Seideloptimal output-sensitive, “ultimate”, hard to implement
Melkmaninput must be a simple polyline

is the number of hull vertices.

Andrew’s monotone chain — write this one

, exact in integers, no angles, no special cases. keepCollinear controls whether collinear boundary points survive on the hull (needed for convex layers and some counting problems).

vector<P> convexHull(vector<P> pts, bool keepCollinear = false) {
    sort(pts.begin(), pts.end());
    pts.erase(unique(pts.begin(), pts.end()), pts.end());
    int n = pts.size();
    if (n <= 2) return pts;
    if (keepCollinear) {
        bool line = true;                              // all points on one line: handle separately
        for (int i = 1; i + 1 < n && line; i++) line = orient(pts[0], pts[n - 1], pts[i]) == 0;
        if (line) return pts;
    }
    auto bad = [&](P a, P b, P p) {                      // should we pop b when adding p?
        int o = orient(a, b, p);
        return keepCollinear ? o < 0 : o <= 0;
    };
    vector<P> h(2 * n);
    int k = 0;
    for (int i = 0; i < n; i++) {                        // lower hull
        while (k >= 2 && bad(h[k - 2], h[k - 1], pts[i])) k--;
        h[k++] = pts[i];
    }
    for (int i = n - 2, lower = k + 1; i >= 0; i--) {     // upper hull
        while (k >= lower && bad(h[k - 2], h[k - 1], pts[i])) k--;
        h[k++] = pts[i];
    }
    h.resize(k - 1);                                     // drop the duplicated start
    return h;                                             // counter-clockwise
}

<= 0 vs < 0 decides whether collinear points on the hull’s edges are kept — get it backwards and you either lose required points or produce a hull with redundant vertices that breaks later queries.

What the hull unlocks

Once the points are in convex position and ordered, many queries become or :

QueryMethod
Diameter (farthest pair)rotating calipers, — 07-rotating-calipers
Width (minimum strip)rotating calipers
Minimum-area enclosing rectanglerotating calipers — one side lies on a hull edge
Point in convex polygonbinary search, — 04-polygons
Extreme point in a directionternary search on the hull, — 08-convex-polygon-queries
Tangents from an external pointbinary search,
Distance between two convex polygonsrotating calipers or Minkowski difference — 10-minkowski-sum
Closest pair of hull verticesnot necessarily on the hull — use divide and conquer, 16-closest-pair
Maximum-area trianglerotating calipers,
Convex hull trick (DP)the upper hull of lines — 14-convex-hull-trick

Degenerate cases to test

The inputs that break hull code

  • and
  • all points identical
  • all points collinear
  • duplicate points in the input
  • exactly three points, collinear

The monotone chain above handles all of these via the early return, the unique call, and the keepCollinear all-on-one-line branch — but only because they are there. Test them.

Dynamic convex hull

Maintaining a hull under insertions and deletions needs a balanced BST of hull vertices, per update (06-dynamic-hull). In contests this is nearly always avoidable:

  • insertions only, offline sort and rebuild;
  • queries offline segment tree on time + a rebuilt hull per node;
  • only need extreme points Li Chao tree on the dual lines.

3D convex hull

The incremental algorithm is ; randomised incremental with a conflict graph is expected. Substantially harder than 2D — the output is a set of faces, and degeneracies (coplanar points) are much nastier. See 19-3d-geometry.

Worth knowing: the 3D hull of points lifted onto a paraboloid has a lower hull that projects exactly to the Delaunay triangulation — the same lifting trick as the in-circle predicate.

Graham Scan

Compute the hull by sorting around a fixed pivot by polar angle instead of by . Historically first; monotone chain is simpler to get right, but Graham scan generalises better to “sweep around a point” problems.

Algorithm

  1. Find the point with the lowest (ties: lowest ) — guaranteed to be on the hull. Call it the pivot.
  2. Sort all other points by angle around the pivot (CCW), breaking ties by distance.
  3. Scan in that order, maintaining a stack, popping while the last three points make a non-left turn.
vector<P> grahamScan(vector<P> pts) {
    int n = pts.size();
    if (n < 3) return pts;
    int piv = 0;
    for (int i = 1; i < n; i++)
        if (pts[i].y < pts[piv].y || (pts[i].y == pts[piv].y && pts[i].x < pts[piv].x)) piv = i;
    swap(pts[0], pts[piv]);
    P pivot = pts[0];
    sort(pts.begin() + 1, pts.end(), [&](P a, P b) {
        ll c = cross(pivot, a, b);
        return c != 0 ? c > 0 : norm2(a - pivot) < norm2(b - pivot);
    });
    vector<P> hull;
    for (P p : pts) {
        while (hull.size() >= 2 && cross(hull[hull.size() - 2], hull.back(), p) <= 0) hull.pop_back();
        hull.push_back(p);
    }
    return hull;
}

Tested: both convexHull and grahamScan are checked against a brute-force hull (a point is a hull vertex iff some directed line through two points has all others strictly on one side or collinear) on random small point sets, including all-collinear and duplicate-point inputs.

Melkman

Compute the hull of a simple polyline (a non-self-intersecting path) in , online, with no sorting — the extra structure in a spatially-coherent input is exactly enough to drop the factor that a general point set needs (, since sorting reduces to hull construction).

Maintain the hull as a deque holding the hull vertices in order, with the same vertex at both ends (so the deque represents a closed polygon). For each new point : if it’s inside the current hull (a left turn with both the front pair and the back pair), skip it; otherwise pop non-left turns from the front and from the back, then push onto both ends.

void melkmanAdd(deque<P>& D, P v) {
    if (D.size() >= 3 && cross(D[1] - D[0], v - D[0]) > 0 &&
        cross(D[D.size() - 1] - D[D.size() - 2], v - D[D.size() - 2]) > 0)
        return;                                          // inside the hull
    while (D.size() >= 2 && cross(D[1] - D[0], v - D[0]) <= 0) D.pop_front();
    while (D.size() >= 2 && cross(D[D.size() - 1] - D[D.size() - 2], v - D[D.size() - 2]) <= 0) D.pop_back();
    D.push_front(v);
    D.push_back(v);
}

Seeding: start with D = {p0, p0} (the first point duplicated) and call melkmanAdd for every subsequent point, including the second and third — do not hand-build an initial triangle: the pop/push steps in add build it up correctly on their own, and a hand-built 3-vertex seed checks only two of the triangle’s three edges (the front and back pairs), silently admitting points that violate the third, unchecked edge.

Each point is pushed and popped at most a constant number of times, so total, and the hull is correct after every insertion (online).

Tested against monotone chain on the boundary of random simple polygons (a simple polygon’s boundary is, by definition, a valid simple closed polyline) — fed in polygon order, both produce the same hull vertex set.

When it applies

  • Hull of a simple polygon’s boundary — the canonical use.
  • Hull of a path/trajectory — GPS traces, robot paths, contour following.
  • Preprocessing step — hull of each piece of a polygon before merging.

Convex Layers

Peel the convex hull repeatedly; layer[i] = how many hulls were removed before point ends up on the hull (the point’s “onion depth”).

vector<int> convexLayers(vector<P> pts) {
    map<P, int> layerOf;
    sort(pts.begin(), pts.end());
    pts.erase(unique(pts.begin(), pts.end()), pts.end());
    vector<P> rest = pts;
    for (int layer = 0; !rest.empty(); layer++) {
        vector<P> hull = convexHull(rest, true);          // keepCollinear: a point ON an edge is this layer
        set<P> on(hull.begin(), hull.end());
        vector<P> next;
        for (P p : rest) { if (on.count(p)) layerOf[p] = layer; else next.push_back(p); }
        rest = next;
    }
    vector<int> res(pts.size());
    for (int i = 0; i < (int)pts.size(); i++) res[i] = layerOf[pts[i]];
    return res;
}

Complexity: worst case (one hull per layer, up to layers) — fine for up to a few thousand. An algorithm exists (Chazelle) but is rarely needed in contests. Typical uses: “remove the hull times”, depth of a point in a point set, nested convex polygons.

Chan’s Algorithm

Compute the hull in output-sensitive time — beating plain when the hull is small.

The idea: guess , then combine

Chan’s algorithm marries two hull algorithms: Graham scan (, good on small groups) and Jarvis march / gift wrapping (, output-sensitive but slow per step).

  1. Guess a value for .
  2. Partition the points into groups of size . Hull each group with Graham scan: total.
  3. Run a Jarvis march on the whole point set, but find each next hull vertex by binary searching the tangent from the current point to each small hull — per group, so per hull step.
  4. Stop after steps. If the hull closed, output it; otherwise : double the exponent (, starting from ) and restart.

Step 3’s “binary search the tangent from a point to a convex polygon” is exactly the tangent routine in Convex Polygon Queries, reused once per group per Jarvis-march step. Chan’s algorithm is rarely hand-rolled in a contest (see the tip below), so it is presented here as the composition of those two already-implemented, already-tested pieces (Graham scan and the tangent search) rather than as new standalone code.

The doubling trick

Start at and square it each round: .

In a contest

Andrew’s monotone chain, every time. with is nothing, and the code is twenty lines of integer arithmetic. Chan’s algorithm is worth knowing for the technique — guess the output size, double (or square) it, pay only for the last guess — not for its constant factor in practice.

The transferable technique

“Guess the output size, run a doubling search, and pay only for the last guess” also gives: output-sensitive 3D hulls, selection with unknown -dependent bounds, and any algorithm whose cost is with unknown but verifiable after the fact.

Kirkpatrick-Seidel

The “ultimate convex hull algorithm” (1986) — , output-sensitive and optimal, predating Chan’s algorithm by a decade but considerably harder to implement.

The idea: marriage before conquest

Ordinary divide and conquer splits the input, recurses, then merges — and the merge is where hull algorithms lose time. Kirkpatrick and Seidel invert the order: do the “merge” first, so the recursion only ever works on points that will actually contribute. Building the upper hull:

  1. Find the median -coordinate in (median of medians).
  2. Find the bridge — the single upper-hull edge crossing the vertical line at the median — in via a prune-and-search argument (a 2D linear program).
  3. The bridge’s endpoints are hull vertices; discard every point below the bridge.
  4. Recurse on the surviving points left and right of the median.

Because step 3 discards non-hull points before recursing, the recursion tree has only leaves.

which solves to — matching the lower bound in the algebraic decision tree model, so this (and Chan’s) is optimal.

Kirkpatrick-SeidelChan
Time
Techniqueprune-and-search, bridge findinggrouping + doubling guess
Needsmedian finding, 2D LPGraham scan + binary search
Implementationhardmoderate
Extends to 3Dawkwardlynaturally

Chan’s algorithm achieves the same bound with simpler ingredients, which is why people learn that one. Kirkpatrick-Seidel’s lasting contribution is the prune-and-search paradigm itself — spend linear time discarding a constant fraction of the input, then recurse — which also powers median-of-medians selection, Megiddo’s / Seidel’s linear-time LP, Welzl’s minimum enclosing circle, slope selection, and ham-sandwich cuts.